|
|
本帖最后由 shawn 于 2025-12-24 13:54 编辑
本教程为GMS低版本客户端添加了现代化的实时倒计时显示,包括:
1. 右上角Buff图标持续时间倒计时(剩余buff时间)
2. 快捷栏技能CD倒计时(剩余冷却时间)
3. 自动在分钟/秒之间切换,倒计时在≥1分钟时在左下角显示分钟数,<1分钟时在正中间显示秒数
GMS在v233版本Destiny: Remastered版本(2022/6/15)中首次引入了居中的秒数倒计时显示,以下是对比图:
GMSv92现代化效果图:
GMS Reboot v233+对比参考图:
前置说明:
1. Buff图标持续时间倒计时是在kinoko(https://github.com/iw2d/kinoko_client)或者kaentake(https://github.com/iw2d/kaentake)的实现基础上进行修改的。并且下面的代码中有许多类定义、ztl、宏定义都来自于kinoko。因此,如果你没有在使用这两套client edit的话,你可能需要先花点时间引入相关的部分。
2. 代码中的函数地址或类偏移均为v92版本的硬编码作为例子。如果你要迁移到其他版本,需要自行查找对应的地址。当然,如果你有相应版本idb作为参照的话,这不会是什么太难的事。
3. 附件请解压出UIWindow99.img并放到UI.wz或者Data/UI下(取决于是wz客户端还是img客户端),里面是v233+版本的数字图片。
核心原理:
主要包含两个函数的hook:
1. CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex,用于显示右上角Buff图标的倒计时阴影
2. CUIStatusBar::CQuickSlot: rawSkillCooltime,用于显示快捷栏技能图标的冷却倒计时阴影
在重写原函数或直接调用原函数后,通过调用客户端的draw_number_by_image方法实现数字绘制
其他细节:
1. 起初在快捷栏绘制的数字会产生叠加,直到下一帧阴影才会重置。CUIStatusBar::CQuickSlot: rawSkillCooltime在v95的pdb中可以看到if ( v21 >= 0 && *v27 != v21 ),说明倒计时阴影只有当下标发生变化时才会copy canvas。因此额外添加了re: atchNop(0x0085136B, 6);跳过了阴影帧下标的检查,这样游戏每帧画面都能刷新技能图标的绘制。
2. 每个技能的冷却时间实际上存放在CWvsContext::m_mSkillCooltimeOver这个哈希表中,通过ZMap<long, long, long>::GetAt方法可以查找哈希表,key是技能ID,value是冷却结束时间戳(以毫秒为单位)。这个时间戳其实是自电脑启动以来经过的毫秒数,即timeGetTime(),由于是DWORD实际上会在49天后loop back,如何修复(或者说缓解)这个漏洞就是另一回事了。
3. 各个类的成员变量偏移可以自行对照v95的pdb寻找,值得注意的是FUNCKEY_MAPPED的nID实际上在0x1偏移处,而不是0x0。所以m_aFuncKeyMappedInfo的偏移是要在对应汇编的那行中额外减去1的。
接下来是代码部分:
CountdownDisplay.h
- #pragma once
- #include "ztl/ztl.h"
- namespace maple {
- namespace countdown_display {
- // Global countdown display resources (for buff duration and skill cooldown)
- extern IWzPropertyPtr g_pPropSecond;
- extern IWzPropertyPtr g_pPropMinute;
- // Attach hooks
- void Attach();
- } // namespace countdown_display
- } // namespace maple
复制代码
CountdownDisplay.cpp
- #include "pch.h"
- #include "mods/CountdownDisplay.h"
- #include "tools/hook.h"
- #include "client/CTemporaryStatView.h"
- #include "client/CUIStatusBar.h"
- namespace maple {
- namespace countdown_display {
- // Global countdown display resources (for buff duration and skill cooldown)
- IWzPropertyPtr g_pPropSecond;
- IWzPropertyPtr g_pPropMinute;
- void Attach() {
- ATTACH_HOOK(CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex,
- CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex_hook);
- ATTACH_HOOK(CUIStatusBar::CQuickSlot::DrawSkillCooltime, CUIStatusBar::CQuickSlot::DrawSkillCooltime_hook);
- // Skip shadow index check in original CUIStatusBar::CQuickSlot::DrawSkillCooltime
- re::PatchNop(0x0085136B, 6);
- }
- } // namespace countdown_display
- } // namespace maple
复制代码
CUIStatusBar.h
- #pragma once
- #include "tools/hook.h"
- #include "ztl/ztl.h"
- #include "client/CWnd.h"
- // Define FUNCKEY_MAPPED and the in-object mapped entry layout.
- #pragma pack(push, 1)
- struct FUNCKEY_MAPPED {
- uint8_t nType; // 0x0
- int32_t nID; // 0x1
- };
- struct FUNCKEY_MAPPED_ENTRY {
- FUNCKEY_MAPPED fkm; // 5 bytes
- uint8_t pad[3]; // padding to align to 12 bytes
- int32_t nNumber; // 4 bytes
- };
- #pragma pack(pop)
- static_assert(sizeof(FUNCKEY_MAPPED) == 0x5);
- static_assert(sizeof(FUNCKEY_MAPPED_ENTRY) == 0xC);
- class CUIStatusBar : public CWnd, public TSingleton<CUIStatusBar, 0x00C33A0C> {
- public:
- class CQuickSlot {
- public:
- // m_aFuncKeyMappedInfo: array of 8 entries, each 12 bytes (FUNCKEY_MAPPED + pad + nNumber)
- MEMBER_ARRAY_AT(FUNCKEY_MAPPED_ENTRY, 0x15B9 - 1, m_aFuncKeyMappedInfo, 8) // 00851108 lea ecx, [esi+15B9h] ;
- MEMBER_AT(IWzGr2DLayerPtr, 0x1618, m_pLayerSkillCooltime) // 008510A0 mov ecx, [esi+1618h] ;
- MEMBER_ARRAY_AT(int, 0x165C, m_aFuncKeyMappedSkillCooltime, 8) // 00851102 lea edi, [esi+165Ch] ;
- MEMBER_HOOK(void, 0x00851060, DrawSkillCooltime)
- inline void GetPosByIndex(int nIdx, int* x, int* y) {
- reinterpret_cast<void(__thiscall*)(CQuickSlot*, int, int*, int*)>(0x00850490)(this, nIdx, x, y);
- }
- };
- };
复制代码
CUIStatusBar.cpp
- #include "pch.h"
- #include "client/CUIStatusBar.h"
- #include "client/CWvsContext.h"
- #include "common/Global.h"
- #include "common/Utility.h"
- #include "mods/CountdownDisplay.h"
- #include "ztl/zmap.h"
- #include "tools/debug.h"
- #include <timeapi.h>
- void CUIStatusBar::CQuickSlot::DrawSkillCooltime_hook() {
- using namespace maple::countdown_display;
- DEBUG_LOG("[DrawSkillCooltime_hook] START");
- DrawSkillCooltime(this);
- IWzGr2DLayer* m_pInstance = this->m_pLayerSkillCooltime.GetInterfacePtr();
- IWzCanvasPtr pCanvas = m_pInstance->Getcanvas();
- auto* entries = this->m_aFuncKeyMappedInfo;
- CWvsContext* m_pStr = CWvsContext::GetInstance();
- // Prepare number drawing resources
- if (!g_pPropMinute) {
- g_pPropMinute = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/Basic.img/ItemNo")));
- }
- if (!g_pPropSecond) {
- g_pPropSecond = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/UIWindow99.img/SkillCooldownNumber/2")));
- }
- DWORD currentTime = timeGetTime();
- // Iterate through 8 quickslots
- for (int i = 0; i < 8; ++i) {
- int nSkillID = entries[i].fkm.nID;
- if (nSkillID == 0) {
- continue;
- }
- // Get position for this slot
- int x = 0;
- int y = 0;
- this->GetPosByIndex(i, &x, &y);
- // Query cooldown end time from map
- DWORD nEndTime = 0;
- ZMap_GetAt(&m_pStr->m_mSkillCooltimeOver, &nSkillID, (int*)&nEndTime);
- if (nEndTime <= 0 || nEndTime <= currentTime) {
- continue;
- }
- // Calculate remaining time in milliseconds and draw countdown number
- int nRemainingMs = (int)(nEndTime - currentTime);
- draw_countdown_number(pCanvas, x, y, nRemainingMs, g_pPropMinute, g_pPropSecond);
- }
- DEBUG_LOG("[DrawSkillCooltime_hook] END");
- }
复制代码
CTemporaryStatView.h
- #pragma once
- #include "tools/hook.h"
- #include "client/CUIToolTip.h"
- #include "common/UINT128.h"
- #include "ztl/ztl.h"
- class CTemporaryStatView {
- public:
- enum TEMPORARY_STAT_TYPE {
- TSV_NONE = 0x0,
- TSV_ITEM = 0x1,
- TSV_SKILL = 0x2,
- TSV_ETC = 0x3,
- TSV_PRIVILEGE = 0x4,
- };
- struct TEMPORARY_STAT : public ZRefCounted {
- unsigned char pad0[0x48 - sizeof(ZRefCounted)];
- MEMBER_AT(int, 0x1C, nType)
- MEMBER_AT(int, 0x20, nID)
- MEMBER_AT(IWzGr2DLayerPtr, 0x2C, pLayer)
- MEMBER_AT(IWzGr2DLayerPtr, 0x30, pLayerShadow)
- MEMBER_AT(int, 0x34, nIndexShadow)
- MEMBER_AT(int, 0x38, bNoShadow)
- MEMBER_AT(int, 0x3C, tLeft)
- MEMBER_AT(int, 0x40, tLeftUnit)
- MEMBER_HOOK(void, 0x0073DB90, UpdateShadowIndex)
- };
- ZList<ZRef<TEMPORARY_STAT>> m_lTemporaryStat;
- CTemporaryStatView() = default;
- virtual ~CTemporaryStatView() = default;
- };
- static_assert(sizeof(CTemporaryStatView) == 0x18);
- static_assert(offsetof(CTemporaryStatView, m_lTemporaryStat) == 0x4);
复制代码
CTemporaryStatView.cpp
- #include "pch.h"
- #include "client/CTemporaryStatView.h"
- #include "client/CUIToolTip.h"
- #include "mods/Resolution.h"
- #include "mods/CountdownDisplay.h"
- #include "common/Global.h"
- #include "common/UINT128.h"
- #include "common/Utility.h"
- #include "tools/debug.h"
- using namespace maple::countdown_display;
- void CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex_hook() {
- DEBUG_LOG("[UpdateShadowIndex_hook] START - ID=%d", nID);
- if (bNoShadow) {
- return;
- }
- int nSeconds = tLeft / 1000;
- if (nSeconds == nIndexShadow) {
- return;
- }
- DEBUG_LOG("[UpdateShadowIndex_hook] Seconds=%d", nSeconds);
- // Hijack nIndexShadow to redraw every second
- nIndexShadow = nSeconds;
- int nShadowIndex = 0;
- if (tLeftUnit) {
- nShadowIndex = tLeft / tLeftUnit;
- if (nShadowIndex < 0) {
- nShadowIndex = 0;
- } else if (nShadowIndex > 15) {
- nShadowIndex = 15;
- }
- }
- // Remove old canvas
- pLayerShadow->RemoveCanvas(-2);
- // Resolve shadow canvas
- wchar_t sShadowProperty[1024];
- swprintf_s(sShadowProperty, 1024, L"UI/UIWindow.img/Skill/CoolTime/%d", nShadowIndex);
- IWzCanvasPtr pShadowCanvas = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(sShadowProperty)));
- // Create new canvas, copy shadow
- IWzCanvasPtr pNewCanvas;
- PcCreateObject<IWzCanvasPtr>(L"Canvas", pNewCanvas, nullptr);
- pNewCanvas->Create(32, 32);
- pNewCanvas->Copy(0, 0, pShadowCanvas);
- // Draw number on canvas
- if (!g_pPropMinute) {
- g_pPropMinute = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/Basic.img/ItemNo")));
- }
- if (!g_pPropSecond) {
- g_pPropSecond = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/UIWindow99.img/SkillCooldownNumber/2")));
- }
- draw_countdown_number(pNewCanvas, 0, 0, tLeft, g_pPropMinute, g_pPropSecond);
- // Insert canvas
- pLayerShadow->InsertCanvas(pNewCanvas, 500, 210, 64);
- DEBUG_LOG("[UpdateShadowIndex_hook] END");
- }
复制代码
Utility.h
- #pragma once
- #include "ztl/ztl.h"
- #include <cmath>
- // Helper function to get int32 from IWzProperty variant
- inline int get_int32(Ztl_variant_t& v, int nDefault) {
- Ztl_variant_t vInt;
- if (V_VT(&v) == VT_EMPTY || V_VT(&v) == VT_ERROR || FAILED(ZComAPI::ZComVariantChangeType(&vInt, &v, 0, VT_I4))) {
- return nDefault;
- } else {
- return V_I4(&vInt);
- }
- }
- inline IUnknownPtr get_unknown(Ztl_variant_t& v) {
- IUnknownPtr result;
- reinterpret_cast<IUnknownPtr*(__cdecl*)(IUnknownPtr*, Ztl_variant_t*)>(0x00418780)(std::addressof(result), &v);
- return result;
- }
- inline int draw_number_by_image(IWzCanvasPtr pCanvas, int nLeft, int nTop, int nNo, IWzPropertyPtr pBase,
- int nHorzSpace) {
- return reinterpret_cast<int(__cdecl*)(IWzCanvasPtr, int, int, int, IWzPropertyPtr, int)>(0x0093EDA0)(
- pCanvas, nLeft, nTop, nNo, pBase, nHorzSpace);
- }
- // Draws countdown number on canvas based on remaining milliseconds
- // Returns true if successfully drawn, false if out of range
- inline bool draw_countdown_number(IWzCanvasPtr pCanvas, int x, int y, int nMillisecondsLeft, IWzPropertyPtr pPropMinute,
- IWzPropertyPtr pPropSecond) {
- int nSeconds = static_cast<int>(std::ceil(nMillisecondsLeft / 1000.0));
- int displayValue;
- IWzPropertyPtr displayUnit;
- int nLeft;
- int nTop;
- if (nSeconds >= 60) {
- displayValue = nSeconds / 60; // Display minutes
- displayUnit = pPropMinute;
- nLeft = 2;
- nTop = 19;
- } else {
- displayValue = nSeconds;
- displayUnit = pPropSecond;
- if (nSeconds >= 10) {
- nLeft = 6;
- nTop = 10;
- } else {
- nLeft = 11;
- nTop = 10;
- }
- }
- if (displayValue <= 0 || displayValue > 999) {
- return false;
- }
- draw_number_by_image(pCanvas, x + nLeft, y + nTop, displayValue, displayUnit, 0);
- return true;
- }
复制代码
zmap.h
- #pragma once
- // ZMap<long, long, long>::GetAt wrapper
- inline int* ZMap_GetAt(void* pThis, const int* key, int* value) {
- return reinterpret_cast<int*(__thiscall*)(void*, const int*, int*)>(0x0047A6A0)(pThis, key, value);
- }
复制代码 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
评分
-
查看全部评分
|