-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMirrorTimer.cpp
More file actions
325 lines (286 loc) · 12.2 KB
/
MirrorTimer.cpp
File metadata and controls
325 lines (286 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetMirrorTimerInfo(index)` / `GetMirrorTimerProgress(label)` — modern
// (3.0+) readers for the BREATH / FATIGUE / EXHAUSTION / FEIGNDEATH
// timer state. Vanilla 1.12's engine fires `MIRROR_TIMER_START` /
// `MIRROR_TIMER_PAUSE` / `MIRROR_TIMER_STOP` events with the full
// packet payload, but doesn't cache anything internally — so we hook
// the SMSG handler (`FUN_005E7990`) and build a 3-slot side cache.
//
// The hook peeks the packet via cursor save/restore on the CDataStore
// (offset `+0x14`) before the original consumes the bytes. After
// peeking, we restore the cursor and let the original handler run
// normally, which fires the existing Lua events untouched.
//
// Timer types in vanilla 1.12 (per `FUN_005E7AE0`):
// 0 → "EXHAUSTION" (sliding outside playable bounds; map edges)
// 1 → "BREATH" (underwater drowning)
// 2 → "FEIGNDEATH" (Hunter Feign Death duration)
//
// Modern uses "FATIGUE" instead of "EXHAUSTION" for the same concept;
// we preserve the engine's wording. Addons that hardcode "FATIGUE"
// from backported code need to handle either.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
#include <windows.h>
namespace Unit::MirrorTimer {
namespace {
using DataStoreReadU32_t = void(__thiscall *)(void *self, uint32_t *out);
using DataStoreReadU8_t = void(__thiscall *)(void *self, uint8_t *out);
using TimerTypeName_t = const char *(__fastcall *)(int type);
using TimerLabel_t = const char *(__fastcall *)(int type, int spellID);
// Windows `GetTickCount` rather than the engine's `FUN_ENGINE_TICK_MS`
// — the engine helper has two internal branches (`GetTickCount` vs a
// high-res counter with a different epoch) selected by a runtime flag,
// and the caller chain runs the result through `__ftol`, suggesting at
// least one path returns via the FPU rather than EAX. Reading those
// bytes as a plain `uint64_t` return picks up garbage. `GetTickCount`
// is single-epoch (ms since boot), monotonic, and unambiguous.
uint32_t NowMs() {
return ::GetTickCount();
}
const char *TypeName(int type) {
auto fn = reinterpret_cast<TimerTypeName_t>(Offsets::FUN_MIRROR_TIMER_TYPE_NAME);
return fn(type);
}
const char *LabelFor(int type, int spellID) {
auto fn = reinterpret_cast<TimerLabel_t>(Offsets::FUN_MIRROR_TIMER_LABEL);
return fn(type, spellID);
}
struct Slot {
bool active = false;
int type = 0;
int initialValue = 0; // value as received in the START packet (ms)
int maxValue = 0;
int scale = 0; // packet rate (positive = decreasing)
bool paused = false;
int spellID = 0; // populated for FEIGNDEATH; 0 otherwise
uint32_t baseMs = 0; // engine ms at which `initialValue` was true
};
Slot g_slots[Offsets::MIRROR_TIMER_SLOT_COUNT];
// Compute the live current value given a slot's snapshot + a current
// engine tick. Matches what the vanilla FrameXML breath/fatigue bars
// do internally: linear interpolation `value = initial + elapsed *
// scale`. Vanilla sends `scale = -1` for depleting timers (breath
// drains 60000 → 0 over 60s = `+ 25000 * -1 = -25000` per 25s
// real-time), so the sign in the formula is `+`, not `-`. Modern's
// docs describe positive scale as depleting; that's a post-vanilla
// convention flip — we follow the wire format the 1.12 server
// actually sends. Returns the snapshot verbatim while paused.
int LiveValue(const Slot &s, uint32_t now) {
if (!s.active)
return 0;
if (s.paused)
return s.initialValue;
const int elapsed = static_cast<int>(now - s.baseMs);
const int value = s.initialValue + elapsed * s.scale;
if (value < 0)
return 0;
if (s.maxValue > 0 && value > s.maxValue)
return s.maxValue;
return value;
}
void OnStart(int type, int value, int maxValue, int scale,
bool paused, int spellID) {
if (type < 0 || type >= Offsets::MIRROR_TIMER_SLOT_COUNT)
return;
Slot &s = g_slots[type];
s.active = true;
s.type = type;
s.initialValue = value;
s.maxValue = maxValue;
s.scale = scale;
s.paused = paused;
s.spellID = spellID;
s.baseMs = NowMs();
}
void OnPause(int type, bool paused) {
if (type < 0 || type >= Offsets::MIRROR_TIMER_SLOT_COUNT)
return;
Slot &s = g_slots[type];
if (!s.active)
return;
if (paused) {
// Freeze: snapshot the live value as the new initial, and
// mark paused so `LiveValue` returns it verbatim.
s.initialValue = LiveValue(s, NowMs());
s.paused = true;
} else if (s.paused) {
// Unpause: reset the base tick so live interpolation
// resumes from the frozen value.
s.paused = false;
s.baseMs = NowMs();
}
}
void OnStop(int type) {
if (type < 0 || type >= Offsets::MIRROR_TIMER_SLOT_COUNT)
return;
g_slots[type] = Slot{};
}
// Peek the packet by reading the fields out of the data store, then
// rewind the cursor so the original handler reads the same bytes.
// `out`-only — caches into our slot table; returns whether the read
// successfully observed the expected fields.
void PeekStartPacket(void *dataStore) {
auto readU32 = reinterpret_cast<DataStoreReadU32_t>(
Offsets::FUN_DATASTORE_READ_U32);
auto readU8 = reinterpret_cast<DataStoreReadU8_t>(
Offsets::FUN_DATASTORE_READ_U8);
auto cursorPtr = reinterpret_cast<uint32_t *>(
static_cast<uint8_t *>(dataStore) + Offsets::OFF_DATASTORE_CURSOR);
const uint32_t saved = *cursorPtr;
uint32_t type = 0, value = 0, maxValue = 0, scale = 0, spellID = 0;
uint8_t paused = 0;
readU32(dataStore, &type);
readU32(dataStore, &value);
readU32(dataStore, &maxValue);
readU32(dataStore, &scale);
readU8(dataStore, &paused);
readU32(dataStore, &spellID);
*cursorPtr = saved;
OnStart(static_cast<int>(type), static_cast<int>(value),
static_cast<int>(maxValue), static_cast<int>(scale),
paused != 0, static_cast<int>(spellID));
}
void PeekPausePacket(void *dataStore) {
auto readU32 = reinterpret_cast<DataStoreReadU32_t>(
Offsets::FUN_DATASTORE_READ_U32);
auto readU8 = reinterpret_cast<DataStoreReadU8_t>(
Offsets::FUN_DATASTORE_READ_U8);
auto cursorPtr = reinterpret_cast<uint32_t *>(
static_cast<uint8_t *>(dataStore) + Offsets::OFF_DATASTORE_CURSOR);
const uint32_t saved = *cursorPtr;
uint32_t type = 0;
uint8_t paused = 0;
readU32(dataStore, &type);
readU8(dataStore, &paused);
*cursorPtr = saved;
OnPause(static_cast<int>(type), paused != 0);
}
void PeekStopPacket(void *dataStore) {
auto readU32 = reinterpret_cast<DataStoreReadU32_t>(
Offsets::FUN_DATASTORE_READ_U32);
auto cursorPtr = reinterpret_cast<uint32_t *>(
static_cast<uint8_t *>(dataStore) + Offsets::OFF_DATASTORE_CURSOR);
const uint32_t saved = *cursorPtr;
uint32_t type = 0;
readU32(dataStore, &type);
*cursorPtr = saved;
OnStop(static_cast<int>(type));
}
// Slot lookup by type-name string. Used by `GetMirrorTimerProgress`,
// whose caller passes the engine's literal name (e.g. "BREATH").
const Slot *FindSlotByName(const char *name) {
if (name == nullptr)
return nullptr;
for (int i = 0; i < Offsets::MIRROR_TIMER_SLOT_COUNT; ++i) {
const Slot &s = g_slots[i];
if (!s.active)
continue;
const char *engineName = TypeName(s.type);
if (engineName != nullptr && std::strcmp(engineName, name) == 0)
return &s;
}
return nullptr;
}
// `GetMirrorTimerInfo(index)` — returns `(timer, value, maxValue,
// scale, paused, label)` for the timer in slot `index` (1..3), or
// nothing if that slot is empty. Matches modern's 6-tuple shape.
// `value` is the snapshot from the last server packet (not
// live-interpolated) — modern documents the same "possibly outdated"
// caveat. Use `GetMirrorTimerProgress(timer)` for the live value.
int __fastcall Script_GetMirrorTimerInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: GetMirrorTimerInfo(index)");
return 0;
}
const int index1 = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIdx = index1 - 1;
if (slotIdx < 0 || slotIdx >= Offsets::MIRROR_TIMER_SLOT_COUNT)
return 0;
const Slot &s = g_slots[slotIdx];
if (!s.active)
return 0;
const char *name = TypeName(s.type);
const char *label = LabelFor(s.type, s.spellID);
Game::Lua::PushString(L, name != nullptr ? name : "");
Game::Lua::PushNumber(L, static_cast<double>(s.initialValue));
Game::Lua::PushNumber(L, static_cast<double>(s.maxValue));
Game::Lua::PushNumber(L, static_cast<double>(s.scale));
Game::Lua::PushBool(L, s.paused);
Game::Lua::PushString(L, label != nullptr ? label : "");
return 6;
}
// `GetMirrorTimerProgress(label)` — returns the live current value of
// the timer matching `label` (one of the engine's type-name strings:
// "EXHAUSTION", "BREATH", "FEIGNDEATH"), in milliseconds. Returns 0
// when no matching timer is currently active. Computed each call by
// linearly interpolating from the last packet snapshot using the
// engine's millisecond clock — same logic the stock FrameXML bars
// run in their `OnUpdate` handlers.
int __fastcall Script_GetMirrorTimerProgress(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: GetMirrorTimerProgress(\"timer\")");
return 0;
}
const char *name = Game::Lua::ToString(L, 1);
const Slot *s = FindSlotByName(name);
if (s == nullptr) {
Game::Lua::PushNumber(L, 0);
return 1;
}
Game::Lua::PushNumber(L, static_cast<double>(LiveValue(*s, NowMs())));
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetMirrorTimerInfo",
&Script_GetMirrorTimerInfo);
Game::Lua::RegisterGlobalFunction("GetMirrorTimerProgress",
&Script_GetMirrorTimerProgress);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
// `FUN_005E7990` is a true `__fastcall` with 4 real args — `param_1` in
// ECX, the opcode (`param_2`) in EDX, then `param_3` and `param_4` on the
// stack. NOT a thiscall — there's no dummy EDX slot. Declaring it with
// the thiscall shape (extra `void *edx` parameter) shifts every
// subsequent arg by one slot, leaving `dataStore` reading garbage off the
// end of the stack frame and crashing inside `FUN_00418E30`'s
// vtable-fallback path when the cursor bounds check fails.
using SMSGHandler_t = int(__fastcall *)(void *param_1, int opcode,
void *param_3, void *dataStore);
SMSGHandler_t SMSGHandler_o = nullptr;
int __fastcall SMSGHandler_h(void *param_1, int opcode,
void *param_3, void *dataStore) {
if (dataStore != nullptr) {
switch (opcode) {
case Offsets::SMSG_OPCODE_MIRROR_TIMER_START:
PeekStartPacket(dataStore);
break;
case Offsets::SMSG_OPCODE_MIRROR_TIMER_PAUSE:
PeekPausePacket(dataStore);
break;
case Offsets::SMSG_OPCODE_MIRROR_TIMER_STOP:
PeekStopPacket(dataStore);
break;
}
}
return SMSGHandler_o(param_1, opcode, param_3, dataStore);
}
static const Game::HookAutoRegister _hookreg{
Offsets::FUN_SMSG_MIRROR_TIMER_HANDLER,
reinterpret_cast<void *>(&SMSGHandler_h),
reinterpret_cast<void **>(&SMSGHandler_o)};
} // namespace Unit::MirrorTimer