mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
The repo was internally inconsistent: the LICENSE file is GPL v3 but every src/ header carried the LGPL v3 notice and the README said "LGPL v3 or later". LGPL was also the wrong fit — this is an injected mod plus a bundled addon, not a library other code links against, so LGPL's linking exception buys nothing. It's also required: src/voicechat/Tts.cpp is ported from awesome_wotlk's VoiceChat.cpp, which is GPL v3, and a derivative of GPL code cannot be relicensed under more permissive terms. Dependencies stay compatible — MinHook (BSD-2) and the nampower-mirrored packet shapes (BSD) are GPL-compatible, and SAPI is covered by the system-library exception. Swept "GNU Lesser General Public License" -> "GNU General Public License" across all 257 src/ headers and changed the README license line to match the GPL v3 LICENSE file. Comment-only; no build impact.
198 lines
7.8 KiB
C++
198 lines
7.8 KiB
C++
// 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 "Game.h"
|
|
#include "event/Custom.h"
|
|
|
|
#include <cstdint>
|
|
|
|
#include <windows.h>
|
|
|
|
// `IsLeft/RightShiftKeyDown` / `IsLeft/RightControlKeyDown` /
|
|
// `IsLeft/RightAltKeyDown` / `IsModifierKeyDown` and the
|
|
// `MODIFIER_STATE_CHANGED(key, down)` event.
|
|
//
|
|
// 1.12's own modifier-key path collapses left and right into a single
|
|
// shift/ctrl/alt boolean (the engine's `IsShiftKeyDown` chain queries
|
|
// `GetKeyState(VK_SHIFT)` — `0x10`, the L+R-merged virtual key — at
|
|
// `0x00436024`, never the L/R-distinguishing `VK_LSHIFT` / `VK_RSHIFT`
|
|
// virtual keys). The information physically exists at the OS level but
|
|
// the engine doesn't expose it.
|
|
//
|
|
// We capture L/R distinction by installing a `WH_GETMESSAGE` hook on
|
|
// our thread (which is the engine's main message-pump thread). The
|
|
// hook intercepts every message dequeued by `GetMessage` /
|
|
// `PeekMessage` *before* it's dispatched — including
|
|
// `WM_KEY{,SYS}{DOWN,UP}`. The VK code in `wParam` plus the scancode +
|
|
// extended-key bit in `lParam` uniquely identify L vs R for each
|
|
// modifier; we maintain a cached 6-bit bitmap that the seven query
|
|
// functions read, and a transition (old != new) fires
|
|
// `MODIFIER_STATE_CHANGED`.
|
|
//
|
|
// Why a thread-message hook instead of a `SetWindowLongPtr` subclass:
|
|
// the engine recreates its main window on some renderer state changes
|
|
// (verified empirically — toggling vsync makes a previously-installed
|
|
// `WNDPROC` subclass go silent, because the new HWND has its own
|
|
// pristine WNDPROC and our subclass is left dangling on a destroyed
|
|
// window). A thread-level hook attaches to the message *thread*, not a
|
|
// specific HWND, so it survives any number of window recreations.
|
|
|
|
namespace Input::Modifier {
|
|
|
|
namespace {
|
|
|
|
constexpr int BIT_LSHIFT = 0;
|
|
constexpr int BIT_RSHIFT = 1;
|
|
constexpr int BIT_LCTRL = 2;
|
|
constexpr int BIT_RCTRL = 3;
|
|
constexpr int BIT_LALT = 4;
|
|
constexpr int BIT_RALT = 5;
|
|
constexpr uint32_t MASK_ANY_MODIFIER = 0x3F;
|
|
|
|
const char *const kKeyName[6] = {
|
|
"LSHIFT", "RSHIFT", "LCTRL", "RCTRL", "LALT", "RALT",
|
|
};
|
|
|
|
constexpr const char *kModifierStateChanged = "MODIFIER_STATE_CHANGED";
|
|
|
|
// Static-init reservation — runs before DllMain. The
|
|
// `RebuildEventTable` hook appends our name to the engine's event-
|
|
// table input when the engine populates it.
|
|
const Event::Custom::AutoReserve _reserveModifierStateChanged{kModifierStateChanged};
|
|
|
|
uint32_t g_modifierMask = 0;
|
|
HHOOK g_msgHook = nullptr;
|
|
|
|
void PushDown(void *L, bool down) {
|
|
if (down)
|
|
Game::Lua::PushNumber(L, 1.0);
|
|
else
|
|
Game::Lua::PushNil(L);
|
|
}
|
|
|
|
template <int BitIdx>
|
|
int __fastcall Script_IsKeyDown(void *L) {
|
|
const bool down = (g_modifierMask & (1u << BitIdx)) != 0;
|
|
PushDown(L, down);
|
|
return 1;
|
|
}
|
|
|
|
int __fastcall Script_IsModifierKeyDown(void *L) {
|
|
const bool down = (g_modifierMask & MASK_ANY_MODIFIER) != 0;
|
|
PushDown(L, down);
|
|
return 1;
|
|
}
|
|
|
|
// Decode the L/R bit position from a key message.
|
|
// - `VK_SHIFT` (the merged virtual key the OS sends for either key)
|
|
// resolves via `MapVirtualKeyA(scancode, MAPVK_VSC_TO_VK_EX)` —
|
|
// the only reliable way to distinguish LSHIFT/RSHIFT from a
|
|
// keyboard message, since shift has no extended-key bit.
|
|
// - `VK_CONTROL` / `VK_MENU` use the `KF_EXTENDED` (bit 24) flag in
|
|
// `lParam`: extended = right key, plain = left key. (Some
|
|
// keyboards' AltGr generates RMENU + LCONTROL; we accept that as-is
|
|
// since it's how the OS reports the keystate.)
|
|
// - The OS occasionally delivers VK_L*/VK_R* directly (e.g. via
|
|
// `SendInput`-injected synthetic events); accept them too.
|
|
// Returns -1 for non-modifier keys.
|
|
int VkToBitIdx(WPARAM wParam, LPARAM lParam) {
|
|
const UINT vk = static_cast<UINT>(wParam);
|
|
const UINT scancode = (lParam >> 16) & 0xFF;
|
|
const bool extended = ((lParam >> 24) & 1) != 0;
|
|
|
|
if (vk == VK_SHIFT) {
|
|
const UINT actual = MapVirtualKeyA(scancode, 3 /* MAPVK_VSC_TO_VK_EX */);
|
|
if (actual == VK_LSHIFT) return BIT_LSHIFT;
|
|
if (actual == VK_RSHIFT) return BIT_RSHIFT;
|
|
return BIT_LSHIFT; // best-effort fallback; treat as left
|
|
}
|
|
if (vk == VK_LSHIFT) return BIT_LSHIFT;
|
|
if (vk == VK_RSHIFT) return BIT_RSHIFT;
|
|
if (vk == VK_CONTROL) return extended ? BIT_RCTRL : BIT_LCTRL;
|
|
if (vk == VK_LCONTROL) return BIT_LCTRL;
|
|
if (vk == VK_RCONTROL) return BIT_RCTRL;
|
|
if (vk == VK_MENU) return extended ? BIT_RALT : BIT_LALT;
|
|
if (vk == VK_LMENU) return BIT_LALT;
|
|
if (vk == VK_RMENU) return BIT_RALT;
|
|
return -1;
|
|
}
|
|
|
|
void ProcessKeyMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
const int bitIdx = VkToBitIdx(wParam, lParam);
|
|
if (bitIdx < 0)
|
|
return;
|
|
|
|
const uint32_t bit = 1u << bitIdx;
|
|
const bool wasDown = (g_modifierMask & bit) != 0;
|
|
const bool nowDown = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
|
if (nowDown == wasDown)
|
|
return;
|
|
|
|
if (nowDown) g_modifierMask |= bit;
|
|
else g_modifierMask &= ~bit;
|
|
|
|
// Slot was assigned when the engine populated its event table; our
|
|
// hook on `RebuildEventTable` recorded the index. Lookup at fire
|
|
// time rather than caching because `/reload` re-runs the rebuild
|
|
// and the index may shift.
|
|
const int slot = Event::Custom::Lookup(kModifierStateChanged);
|
|
if (slot >= 0)
|
|
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(nowDown));
|
|
}
|
|
|
|
LRESULT CALLBACK GetMsgHook(int code, WPARAM wParam, LPARAM lParam) {
|
|
// Only process when the message is being removed from the queue
|
|
// (`PM_REMOVE` / `GetMessage`); `PM_NOREMOVE` peeks would deliver
|
|
// the same key twice if we processed both.
|
|
if (code == HC_ACTION && wParam == PM_REMOVE) {
|
|
const MSG *m = reinterpret_cast<const MSG *>(lParam);
|
|
if (m->message == WM_KEYDOWN || m->message == WM_KEYUP ||
|
|
m->message == WM_SYSKEYDOWN || m->message == WM_SYSKEYUP) {
|
|
ProcessKeyMessage(m->message, m->wParam, m->lParam);
|
|
}
|
|
}
|
|
return CallNextHookEx(nullptr, code, wParam, lParam);
|
|
}
|
|
|
|
void InstallHook() {
|
|
if (g_msgHook != nullptr)
|
|
return;
|
|
g_msgHook = SetWindowsHookExA(WH_GETMESSAGE, &GetMsgHook, nullptr,
|
|
GetCurrentThreadId());
|
|
}
|
|
|
|
void RegisterLuaFunctions() {
|
|
Game::Lua::RegisterGlobalFunction("IsLeftShiftKeyDown",
|
|
&Script_IsKeyDown<BIT_LSHIFT>);
|
|
Game::Lua::RegisterGlobalFunction("IsRightShiftKeyDown",
|
|
&Script_IsKeyDown<BIT_RSHIFT>);
|
|
Game::Lua::RegisterGlobalFunction("IsLeftControlKeyDown",
|
|
&Script_IsKeyDown<BIT_LCTRL>);
|
|
Game::Lua::RegisterGlobalFunction("IsRightControlKeyDown",
|
|
&Script_IsKeyDown<BIT_RCTRL>);
|
|
Game::Lua::RegisterGlobalFunction("IsLeftAltKeyDown",
|
|
&Script_IsKeyDown<BIT_LALT>);
|
|
Game::Lua::RegisterGlobalFunction("IsRightAltKeyDown",
|
|
&Script_IsKeyDown<BIT_RALT>);
|
|
Game::Lua::RegisterGlobalFunction("IsModifierKeyDown",
|
|
&Script_IsModifierKeyDown);
|
|
|
|
InstallHook();
|
|
}
|
|
|
|
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
|
|
|
} // namespace
|
|
|
|
} // namespace Input::Modifier
|