找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 72|回复: 1

[客户端] GMS现代化实时倒计时显示(BUFF持续时间和快捷栏冷却时间)

[复制链接]

1

主题

2

回帖

4

积分

新手上路

积分
4
发表于 前天 13:54 | 显示全部楼层 |阅读模式
本帖最后由 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
  1. #pragma once
  2. #include "ztl/ztl.h"

  3. namespace maple {
  4. namespace countdown_display {

  5. // Global countdown display resources (for buff duration and skill cooldown)
  6. extern IWzPropertyPtr g_pPropSecond;
  7. extern IWzPropertyPtr g_pPropMinute;

  8. // Attach hooks
  9. void Attach();

  10. }  // namespace countdown_display
  11. }  // namespace maple
复制代码


CountdownDisplay.cpp
  1. #include "pch.h"
  2. #include "mods/CountdownDisplay.h"
  3. #include "tools/hook.h"
  4. #include "client/CTemporaryStatView.h"
  5. #include "client/CUIStatusBar.h"

  6. namespace maple {
  7. namespace countdown_display {

  8. // Global countdown display resources (for buff duration and skill cooldown)
  9. IWzPropertyPtr g_pPropSecond;
  10. IWzPropertyPtr g_pPropMinute;

  11. void Attach() {
  12.     ATTACH_HOOK(CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex,
  13.                 CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex_hook);

  14.     ATTACH_HOOK(CUIStatusBar::CQuickSlot::DrawSkillCooltime, CUIStatusBar::CQuickSlot::DrawSkillCooltime_hook);

  15.     // Skip shadow index check in original CUIStatusBar::CQuickSlot::DrawSkillCooltime
  16.     re::PatchNop(0x0085136B, 6);
  17. }

  18. }  // namespace countdown_display
  19. }  // namespace maple
复制代码


CUIStatusBar.h
  1. #pragma once
  2. #include "tools/hook.h"
  3. #include "ztl/ztl.h"
  4. #include "client/CWnd.h"

  5. // Define FUNCKEY_MAPPED and the in-object mapped entry layout.
  6. #pragma pack(push, 1)
  7. struct FUNCKEY_MAPPED {
  8.     uint8_t nType;  // 0x0
  9.     int32_t nID;    // 0x1
  10. };

  11. struct FUNCKEY_MAPPED_ENTRY {
  12.     FUNCKEY_MAPPED fkm;  // 5 bytes
  13.     uint8_t pad[3];      // padding to align to 12 bytes
  14.     int32_t nNumber;     // 4 bytes
  15. };
  16. #pragma pack(pop)

  17. static_assert(sizeof(FUNCKEY_MAPPED) == 0x5);
  18. static_assert(sizeof(FUNCKEY_MAPPED_ENTRY) == 0xC);

  19. class CUIStatusBar : public CWnd, public TSingleton<CUIStatusBar, 0x00C33A0C> {
  20. public:
  21.     class CQuickSlot {
  22.     public:
  23.         // m_aFuncKeyMappedInfo: array of 8 entries, each 12 bytes (FUNCKEY_MAPPED + pad + nNumber)
  24.         MEMBER_ARRAY_AT(FUNCKEY_MAPPED_ENTRY, 0x15B9 - 1, m_aFuncKeyMappedInfo, 8)  // 00851108  lea ecx, [esi+15B9h] ;
  25.         MEMBER_AT(IWzGr2DLayerPtr, 0x1618, m_pLayerSkillCooltime)                   // 008510A0  mov ecx, [esi+1618h] ;
  26.         MEMBER_ARRAY_AT(int, 0x165C, m_aFuncKeyMappedSkillCooltime, 8)              // 00851102  lea edi, [esi+165Ch] ;

  27.         MEMBER_HOOK(void, 0x00851060, DrawSkillCooltime)

  28.         inline void GetPosByIndex(int nIdx, int* x, int* y) {
  29.             reinterpret_cast<void(__thiscall*)(CQuickSlot*, int, int*, int*)>(0x00850490)(this, nIdx, x, y);
  30.         }
  31.     };
  32. };
复制代码


CUIStatusBar.cpp
  1. #include "pch.h"
  2. #include "client/CUIStatusBar.h"
  3. #include "client/CWvsContext.h"
  4. #include "common/Global.h"
  5. #include "common/Utility.h"
  6. #include "mods/CountdownDisplay.h"
  7. #include "ztl/zmap.h"
  8. #include "tools/debug.h"
  9. #include <timeapi.h>

  10. void CUIStatusBar::CQuickSlot::DrawSkillCooltime_hook() {
  11.     using namespace maple::countdown_display;

  12.     DEBUG_LOG("[DrawSkillCooltime_hook] START");
  13.     DrawSkillCooltime(this);

  14.     IWzGr2DLayer* m_pInstance = this->m_pLayerSkillCooltime.GetInterfacePtr();
  15.     IWzCanvasPtr pCanvas = m_pInstance->Getcanvas();
  16.     auto* entries = this->m_aFuncKeyMappedInfo;
  17.     CWvsContext* m_pStr = CWvsContext::GetInstance();

  18.     // Prepare number drawing resources
  19.     if (!g_pPropMinute) {
  20.         g_pPropMinute = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/Basic.img/ItemNo")));
  21.     }

  22.     if (!g_pPropSecond) {
  23.         g_pPropSecond = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/UIWindow99.img/SkillCooldownNumber/2")));
  24.     }

  25.     DWORD currentTime = timeGetTime();

  26.     // Iterate through 8 quickslots
  27.     for (int i = 0; i < 8; ++i) {
  28.         int nSkillID = entries[i].fkm.nID;
  29.         if (nSkillID == 0) {
  30.             continue;
  31.         }

  32.         // Get position for this slot
  33.         int x = 0;
  34.         int y = 0;
  35.         this->GetPosByIndex(i, &x, &y);

  36.         // Query cooldown end time from map
  37.         DWORD nEndTime = 0;
  38.         ZMap_GetAt(&m_pStr->m_mSkillCooltimeOver, &nSkillID, (int*)&nEndTime);

  39.         if (nEndTime <= 0 || nEndTime <= currentTime) {
  40.             continue;
  41.         }

  42.         // Calculate remaining time in milliseconds and draw countdown number
  43.         int nRemainingMs = (int)(nEndTime - currentTime);
  44.         draw_countdown_number(pCanvas, x, y, nRemainingMs, g_pPropMinute, g_pPropSecond);
  45.     }

  46.     DEBUG_LOG("[DrawSkillCooltime_hook] END");
  47. }
复制代码


CTemporaryStatView.h
  1. #pragma once
  2. #include "tools/hook.h"
  3. #include "client/CUIToolTip.h"
  4. #include "common/UINT128.h"
  5. #include "ztl/ztl.h"

  6. class CTemporaryStatView {
  7. public:
  8.     enum TEMPORARY_STAT_TYPE {
  9.         TSV_NONE = 0x0,
  10.         TSV_ITEM = 0x1,
  11.         TSV_SKILL = 0x2,
  12.         TSV_ETC = 0x3,
  13.         TSV_PRIVILEGE = 0x4,
  14.     };

  15.     struct TEMPORARY_STAT : public ZRefCounted {
  16.         unsigned char pad0[0x48 - sizeof(ZRefCounted)];
  17.         MEMBER_AT(int, 0x1C, nType)
  18.         MEMBER_AT(int, 0x20, nID)
  19.         MEMBER_AT(IWzGr2DLayerPtr, 0x2C, pLayer)
  20.         MEMBER_AT(IWzGr2DLayerPtr, 0x30, pLayerShadow)
  21.         MEMBER_AT(int, 0x34, nIndexShadow)
  22.         MEMBER_AT(int, 0x38, bNoShadow)
  23.         MEMBER_AT(int, 0x3C, tLeft)
  24.         MEMBER_AT(int, 0x40, tLeftUnit)

  25.         MEMBER_HOOK(void, 0x0073DB90, UpdateShadowIndex)
  26.     };

  27.     ZList<ZRef<TEMPORARY_STAT>> m_lTemporaryStat;

  28.     CTemporaryStatView() = default;
  29.     virtual ~CTemporaryStatView() = default;
  30. };

  31. static_assert(sizeof(CTemporaryStatView) == 0x18);
  32. static_assert(offsetof(CTemporaryStatView, m_lTemporaryStat) == 0x4);
复制代码


CTemporaryStatView.cpp
  1. #include "pch.h"
  2. #include "client/CTemporaryStatView.h"
  3. #include "client/CUIToolTip.h"
  4. #include "mods/Resolution.h"
  5. #include "mods/CountdownDisplay.h"
  6. #include "common/Global.h"
  7. #include "common/UINT128.h"
  8. #include "common/Utility.h"
  9. #include "tools/debug.h"

  10. using namespace maple::countdown_display;

  11. void CTemporaryStatView::TEMPORARY_STAT::UpdateShadowIndex_hook() {
  12.     DEBUG_LOG("[UpdateShadowIndex_hook] START - ID=%d", nID);
  13.     if (bNoShadow) {
  14.         return;
  15.     }
  16.     int nSeconds = tLeft / 1000;
  17.     if (nSeconds == nIndexShadow) {
  18.         return;
  19.     }

  20.     DEBUG_LOG("[UpdateShadowIndex_hook] Seconds=%d", nSeconds);

  21.     // Hijack nIndexShadow to redraw every second
  22.     nIndexShadow = nSeconds;
  23.     int nShadowIndex = 0;
  24.     if (tLeftUnit) {
  25.         nShadowIndex = tLeft / tLeftUnit;
  26.         if (nShadowIndex < 0) {
  27.             nShadowIndex = 0;
  28.         } else if (nShadowIndex > 15) {
  29.             nShadowIndex = 15;
  30.         }
  31.     }

  32.     // Remove old canvas
  33.     pLayerShadow->RemoveCanvas(-2);

  34.     // Resolve shadow canvas
  35.     wchar_t sShadowProperty[1024];
  36.     swprintf_s(sShadowProperty, 1024, L"UI/UIWindow.img/Skill/CoolTime/%d", nShadowIndex);
  37.     IWzCanvasPtr pShadowCanvas = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(sShadowProperty)));

  38.     // Create new canvas, copy shadow
  39.     IWzCanvasPtr pNewCanvas;
  40.     PcCreateObject<IWzCanvasPtr>(L"Canvas", pNewCanvas, nullptr);
  41.     pNewCanvas->Create(32, 32);
  42.     pNewCanvas->Copy(0, 0, pShadowCanvas);

  43.     // Draw number on canvas
  44.     if (!g_pPropMinute) {
  45.         g_pPropMinute = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/Basic.img/ItemNo")));
  46.     }

  47.     if (!g_pPropSecond) {
  48.         g_pPropSecond = get_unknown(get_rm()->GetObjectA(Ztl_bstr_t(L"UI/UIWindow99.img/SkillCooldownNumber/2")));
  49.     }

  50.     draw_countdown_number(pNewCanvas, 0, 0, tLeft, g_pPropMinute, g_pPropSecond);

  51.     // Insert canvas
  52.     pLayerShadow->InsertCanvas(pNewCanvas, 500, 210, 64);
  53.     DEBUG_LOG("[UpdateShadowIndex_hook] END");
  54. }
复制代码


Utility.h
  1. #pragma once
  2. #include "ztl/ztl.h"
  3. #include <cmath>

  4. // Helper function to get int32 from IWzProperty variant
  5. inline int get_int32(Ztl_variant_t& v, int nDefault) {
  6.     Ztl_variant_t vInt;
  7.     if (V_VT(&v) == VT_EMPTY || V_VT(&v) == VT_ERROR || FAILED(ZComAPI::ZComVariantChangeType(&vInt, &v, 0, VT_I4))) {
  8.         return nDefault;
  9.     } else {
  10.         return V_I4(&vInt);
  11.     }
  12. }

  13. inline IUnknownPtr get_unknown(Ztl_variant_t& v) {
  14.     IUnknownPtr result;
  15.     reinterpret_cast<IUnknownPtr*(__cdecl*)(IUnknownPtr*, Ztl_variant_t*)>(0x00418780)(std::addressof(result), &v);
  16.     return result;
  17. }

  18. inline int draw_number_by_image(IWzCanvasPtr pCanvas, int nLeft, int nTop, int nNo, IWzPropertyPtr pBase,
  19.                                 int nHorzSpace) {
  20.     return reinterpret_cast<int(__cdecl*)(IWzCanvasPtr, int, int, int, IWzPropertyPtr, int)>(0x0093EDA0)(
  21.         pCanvas, nLeft, nTop, nNo, pBase, nHorzSpace);
  22. }

  23. // Draws countdown number on canvas based on remaining milliseconds
  24. // Returns true if successfully drawn, false if out of range
  25. inline bool draw_countdown_number(IWzCanvasPtr pCanvas, int x, int y, int nMillisecondsLeft, IWzPropertyPtr pPropMinute,
  26.                                   IWzPropertyPtr pPropSecond) {
  27.     int nSeconds = static_cast<int>(std::ceil(nMillisecondsLeft / 1000.0));

  28.     int displayValue;
  29.     IWzPropertyPtr displayUnit;
  30.     int nLeft;
  31.     int nTop;

  32.     if (nSeconds >= 60) {
  33.         displayValue = nSeconds / 60;  // Display minutes
  34.         displayUnit = pPropMinute;
  35.         nLeft = 2;
  36.         nTop = 19;
  37.     } else {
  38.         displayValue = nSeconds;
  39.         displayUnit = pPropSecond;
  40.         if (nSeconds >= 10) {
  41.             nLeft = 6;
  42.             nTop = 10;
  43.         } else {
  44.             nLeft = 11;
  45.             nTop = 10;
  46.         }
  47.     }

  48.     if (displayValue <= 0 || displayValue > 999) {
  49.         return false;
  50.     }

  51.     draw_number_by_image(pCanvas, x + nLeft, y + nTop, displayValue, displayUnit, 0);
  52.     return true;
  53. }
复制代码


zmap.h
  1. #pragma once

  2. // ZMap<long, long, long>::GetAt wrapper
  3. inline int* ZMap_GetAt(void* pThis, const int* key, int* value) {
  4.     return reinterpret_cast<int*(__thiscall*)(void*, const int*, int*)>(0x0047A6A0)(pThis, key, value);
  5. }
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×

评分

参与人数 1蘑菇币 +50 收起 理由
leevccc + 50 很给力!

查看全部评分

0

主题

18

回帖

385

积分

中级会员

积分
385
发表于 前天 14:40 | 显示全部楼层
TETO这套东西是真的好用 用了之后减少好多代码量
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|小黑屋|蘑菇物语

GMT+8, 2025-12-26 10:08 , Processed in 0.056804 second(s), 25 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表