diff --git a/neo/engine/doomdll.vcxproj.user b/neo/engine/doomdll.vcxproj.user
index 35d47df4..c6d187b5 100644
--- a/neo/engine/doomdll.vcxproj.user
+++ b/neo/engine/doomdll.vcxproj.user
@@ -15,9 +15,9 @@
D:\projects\Doom3\Doom3.exe
WindowsLocalDebugger
- +set r_fullscreen 1 +set r_mode 9
+ +set r_fullscreen 0 +set r_mode 9
D:\projects\Doom3
- +set r_fullscreen 1 +set r_mode 1|+set r_fullscreen 0 +set r_mode 1|+set r_fullscreen 1 +set r_mode 0|+set r_fullscreen 1 +set r_mode 10|+set r_fullscreen 1 +set r_mode 9|
+ +set r_fullscreen 0 +set r_mode 1|+set r_fullscreen 1 +set r_mode 0|+set r_fullscreen 1 +set r_mode 10|+set r_fullscreen 1 +set r_mode 9|+set r_fullscreen 0 +set r_mode 9|
D:\projects\Doom3\Quake4.exe
diff --git a/neo/engine/framework/Licensee.h b/neo/engine/framework/Licensee.h
index 824d6eb9..bc8da52c 100644
--- a/neo/engine/framework/Licensee.h
+++ b/neo/engine/framework/Licensee.h
@@ -63,6 +63,8 @@ If you have questions concerning this license or the applicable additional terms
#ifdef PREY
#define CONFIG_FILE "PreyConfig.cfg"
+#elif defined(QUAKE4)
+#define CONFIG_FILE "Q4Config.cfg"
#else
#define CONFIG_FILE "DoomConfig.cfg"
#endif
diff --git a/neo/engine/idlib/precompiled.h b/neo/engine/idlib/precompiled.h
index f42eec5e..d3b7211e 100644
--- a/neo/engine/idlib/precompiled.h
+++ b/neo/engine/idlib/precompiled.h
@@ -96,7 +96,8 @@ const float MAX_BOUND_SIZE = 65536.0f;
#define DIRECTSOUND_VERSION 0x0800
#include
-#include
+// #include
+#include "../opengl/DirectInputShim.h"
#endif /* !GAME_DLL */
#endif /* !_D3SDK */
diff --git a/neo/engine/opengl/DirectInputShim.cpp b/neo/engine/opengl/DirectInputShim.cpp
new file mode 100644
index 00000000..fc611e21
--- /dev/null
+++ b/neo/engine/opengl/DirectInputShim.cpp
@@ -0,0 +1,1633 @@
+#ifndef DIRECTINPUT_VERSION
+#define DIRECTINPUT_VERSION 0x0800
+#endif
+
+#include "DirectInputShim.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef ICE_DI_DEFAULT_BUFFER_SIZE
+#define ICE_DI_DEFAULT_BUFFER_SIZE 256
+#endif
+
+#ifndef ICE_DI_CENTER_MOUSE_WHEN_ACTIVE
+#define ICE_DI_CENTER_MOUSE_WHEN_ACTIVE 1
+#endif
+
+#ifndef ICE_DI_CENTER_WARP_IGNORE_MS
+#define ICE_DI_CENTER_WARP_IGNORE_MS 120
+#endif
+
+#ifndef ICE_DI_CENTER_PIXEL_TOLERANCE
+#define ICE_DI_CENTER_PIXEL_TOLERANCE 1
+#endif
+
+// Forward declarations
+class IceDirectInputDevice8;
+class IceDirectInput8;
+
+bool IceDirectInputShim_RegisterRawInput(HWND hwnd);
+bool IceDirectInputShim_ForceMouseCenter();
+void IceDirectInputShim_SetMouseTargetWindow(HWND hwnd);
+void IceDirectInputShim_SetMouseCenterLock(bool enabled);
+
+// Globals
+static HWND g_rawInputHwnd = NULL;
+static HWND g_keyboardTargetHwnd = NULL;
+static HWND g_mouseTargetHwnd = NULL;
+static IceDirectInputDevice8* g_keyboardDevice = NULL;
+static IceDirectInputDevice8* g_mouseDevice = NULL;
+static std::mutex g_deviceMutex;
+static std::mutex g_windowMutex;
+static std::atomic g_iceDI_GameMouseActive(true);
+static std::atomic g_iceDI_MouseAcquired(false);
+static std::atomic g_iceDI_CenterMouseWhenActive(ICE_DI_CENTER_MOUSE_WHEN_ACTIVE != 0);
+
+// The shim owns the clip only while the game mouse is active.
+static bool g_mouseClipOwned = false;
+static RECT g_lastClipRect = { 0, 0, 0, 0 };
+
+// Used only to ignore absolute raw-mouse packets generated by our own recentering.
+static POINT g_lastCenterWarpPoint = { 0, 0 };
+static DWORD g_lastCenterWarpTick = 0;
+
+// ------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------
+
+static DWORD IceDI_GetTimeStamp() {
+ return GetTickCount();
+}
+
+static LONG IceDI_AbsLong(LONG v) {
+ return v < 0 ? -v : v;
+}
+
+static bool IceDI_IsDInputProp(REFGUID rguidProp, DWORD propId) {
+ // DirectInput DIPROP_* values are not real GUID objects.
+ // They are encoded as invalid pointer-sized constants:
+ // #define MAKEDIPROP(prop) (*(const GUID *)(prop))
+ //
+ // So do NOT compare DIPROP_* with operator== / IsEqualGUID / memcmp.
+ return (DWORD)(uintptr_t)&rguidProp == propId;
+}
+
+static bool IceDI_IsPressedRawKeyboard(const RAWKEYBOARD& kb) {
+ return (kb.Flags & RI_KEY_BREAK) == 0;
+}
+
+static bool IceDI_IsUsableWindow(HWND hwnd) {
+ return hwnd != NULL && ::IsWindow(hwnd) != FALSE;
+}
+
+static bool IceDI_IsVisibleUsableWindow(HWND hwnd) {
+ return IceDI_IsUsableWindow(hwnd) && ::IsWindowVisible(hwnd) != FALSE;
+}
+
+static bool IceDI_ForegroundOwnsWindow(HWND hwnd) {
+ if (!IceDI_IsUsableWindow(hwnd)) {
+ return false;
+ }
+
+ HWND foreground = ::GetForegroundWindow();
+ if (!IceDI_IsUsableWindow(foreground)) {
+ return false;
+ }
+
+ if (foreground == hwnd) {
+ return true;
+ }
+
+ // Embedded case: the Doom/game HWND can be a child of the editor's
+ // foreground top-level HWND. This is the case that needs live client-to-
+ // screen conversion instead of stale cached x/y/resolution.
+ if (::IsChild(foreground, hwnd)) {
+ return true;
+ }
+
+ HWND root = ::GetAncestor(hwnd, GA_ROOT);
+ if (root != NULL && root == foreground) {
+ return true;
+ }
+
+ // Rare, but allow it when a child of our target became foreground.
+ if (::IsChild(hwnd, foreground)) {
+ return true;
+ }
+
+ return false;
+}
+
+static bool IceDI_GetClientScreenRect(HWND hwnd, RECT* outRect) {
+ if (outRect == NULL || !IceDI_IsUsableWindow(hwnd)) {
+ return false;
+ }
+
+ RECT client;
+ if (!::GetClientRect(hwnd, &client)) {
+ return false;
+ }
+
+ if ((client.right - client.left) <= 0 || (client.bottom - client.top) <= 0) {
+ return false;
+ }
+
+ POINT pts[2];
+ pts[0].x = client.left;
+ pts[0].y = client.top;
+ pts[1].x = client.right;
+ pts[1].y = client.bottom;
+
+ if (!::ClientToScreen(hwnd, &pts[0])) {
+ return false;
+ }
+
+ if (!::ClientToScreen(hwnd, &pts[1])) {
+ return false;
+ }
+
+ outRect->left = std::min(pts[0].x, pts[1].x);
+ outRect->top = std::min(pts[0].y, pts[1].y);
+ outRect->right = std::max(pts[0].x, pts[1].x);
+ outRect->bottom = std::max(pts[0].y, pts[1].y);
+
+ return (outRect->right > outRect->left) && (outRect->bottom > outRect->top);
+}
+
+static bool IceDI_RectEquals(const RECT& a, const RECT& b) {
+ return a.left == b.left && a.top == b.top && a.right == b.right && a.bottom == b.bottom;
+}
+
+static HWND IceDI_GetFallbackRawInputWindow_NoLock() {
+ if (IceDI_IsUsableWindow(g_rawInputHwnd)) {
+ return g_rawInputHwnd;
+ }
+
+ if (IceDI_IsUsableWindow(g_mouseTargetHwnd)) {
+ return g_mouseTargetHwnd;
+ }
+
+ if (IceDI_IsUsableWindow(g_keyboardTargetHwnd)) {
+ return g_keyboardTargetHwnd;
+ }
+
+ return NULL;
+}
+
+static HWND IceDI_GetMouseTargetWindow() {
+ HWND target = NULL;
+ HWND fallback = NULL;
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ target = g_mouseTargetHwnd;
+ fallback = IceDI_GetFallbackRawInputWindow_NoLock();
+ }
+
+ if (IceDI_IsUsableWindow(target)) {
+ return target;
+ }
+
+ if (IceDI_IsUsableWindow(fallback)) {
+ return fallback;
+ }
+
+ // Last-ditch fallback for code paths that acquire before setting
+ // cooperative level. GetFocus only works reliably inside this thread,
+ // but it is better than retaining a stale hwnd.
+ HWND focus = ::GetFocus();
+ if (IceDI_IsUsableWindow(focus)) {
+ return focus;
+ }
+
+ return NULL;
+}
+
+static bool IceDI_ShouldLockMouseToWindow(HWND hwnd) {
+ if (!g_iceDI_CenterMouseWhenActive.load()) {
+ return false;
+ }
+
+ if (!g_iceDI_GameMouseActive.load()) {
+ return false;
+ }
+
+ if (!g_iceDI_MouseAcquired.load()) {
+ return false;
+ }
+
+ if (!IceDI_IsVisibleUsableWindow(hwnd)) {
+ return false;
+ }
+
+ if (!IceDI_ForegroundOwnsWindow(hwnd)) {
+ return false;
+ }
+
+ return true;
+}
+
+static void IceDI_RememberCenterWarp(const POINT& center) {
+ std::lock_guard lock(g_windowMutex);
+ g_lastCenterWarpPoint = center;
+ g_lastCenterWarpTick = IceDI_GetTimeStamp();
+}
+
+static bool IceDI_IsRecentCenterWarpPoint(const POINT& pt) {
+ POINT lastPoint;
+ DWORD lastTick;
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ lastPoint = g_lastCenterWarpPoint;
+ lastTick = g_lastCenterWarpTick;
+ }
+
+ if (lastTick == 0) {
+ return false;
+ }
+
+ DWORD now = IceDI_GetTimeStamp();
+ if ((DWORD)(now - lastTick) > ICE_DI_CENTER_WARP_IGNORE_MS) {
+ return false;
+ }
+
+ return IceDI_AbsLong(pt.x - lastPoint.x) <= 2 && IceDI_AbsLong(pt.y - lastPoint.y) <= 2;
+}
+
+static void IceDI_ClearMouseClipIfOwned() {
+ bool shouldClear = false;
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ shouldClear = g_mouseClipOwned;
+ g_mouseClipOwned = false;
+ g_lastClipRect.left = 0;
+ g_lastClipRect.top = 0;
+ g_lastClipRect.right = 0;
+ g_lastClipRect.bottom = 0;
+ }
+
+ if (shouldClear) {
+ ::ClipCursor(NULL);
+ }
+}
+
+static bool IceDI_ApplyMouseClip(const RECT& rect) {
+ bool needsClip = true;
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ needsClip = !g_mouseClipOwned || !IceDI_RectEquals(g_lastClipRect, rect);
+ g_mouseClipOwned = true;
+ g_lastClipRect = rect;
+ }
+
+ // Calling ClipCursor repeatedly is okay, but avoid doing it every poll when
+ // the rect has not changed. If the editor moves/reparents/resizes the child
+ // window, the live rect changes and this is applied immediately.
+ if (needsClip) {
+ if (!::ClipCursor(&rect)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool IceDI_SkipCenter = false;
+
+void IceDI_ForceOverrideSkipCenter(bool shouldSkip) {
+ IceDI_SkipCenter = shouldSkip;
+}
+
+static bool IceDI_ForceMouseCenterInternal(bool forceWarp) {
+ HWND hwnd = IceDI_GetMouseTargetWindow();
+
+ if (!IceDI_ShouldLockMouseToWindow(hwnd) || IceDI_SkipCenter) {
+ IceDI_ClearMouseClipIfOwned();
+ return false;
+ }
+
+ RECT clientScreenRect;
+ if (!IceDI_GetClientScreenRect(hwnd, &clientScreenRect)) {
+ IceDI_ClearMouseClipIfOwned();
+ return false;
+ }
+
+ POINT center;
+ center.x = clientScreenRect.left + ((clientScreenRect.right - clientScreenRect.left) / 2);
+ center.y = clientScreenRect.top + ((clientScreenRect.bottom - clientScreenRect.top) / 2);
+
+ IceDI_ApplyMouseClip(clientScreenRect);
+
+ POINT cursor;
+ bool haveCursor = (::GetCursorPos(&cursor) != FALSE);
+ bool outside = !haveCursor || !::PtInRect(&clientScreenRect, cursor);
+ bool offCenter = !haveCursor ||
+ IceDI_AbsLong(cursor.x - center.x) > ICE_DI_CENTER_PIXEL_TOLERANCE ||
+ IceDI_AbsLong(cursor.y - center.y) > ICE_DI_CENTER_PIXEL_TOLERANCE;
+
+ if (forceWarp || outside || offCenter) {
+ IceDI_RememberCenterWarp(center);
+ ::SetCursorPos(center.x, center.y);
+ }
+
+ return true;
+}
+
+static bool IceDI_RawAbsoluteMouseToScreen(const RAWMOUSE& rm, POINT* outPoint) {
+ if (outPoint == NULL) {
+ return false;
+ }
+
+ int left = 0;
+ int top = 0;
+ int width = ::GetSystemMetrics(SM_CXSCREEN);
+ int height = ::GetSystemMetrics(SM_CYSCREEN);
+
+ if (rm.usFlags & MOUSE_VIRTUAL_DESKTOP) {
+ left = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
+ top = ::GetSystemMetrics(SM_YVIRTUALSCREEN);
+ width = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
+ height = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
+ }
+
+ if (width <= 0 || height <= 0) {
+ return false;
+ }
+
+ // RAWMOUSE absolute packets are normalized to 0..65535 over the selected
+ // desktop rectangle. Treating these values as relative deltas is a classic
+ // cause of huge mouse jumps/skips.
+ outPoint->x = left + ::MulDiv(rm.lLastX, width - 1, 65535);
+ outPoint->y = top + ::MulDiv(rm.lLastY, height - 1, 65535);
+ return true;
+}
+
+static DWORD IceDI_RawKeyboardToDIK(const RAWKEYBOARD& kb) {
+ USHORT makeCode = kb.MakeCode;
+ USHORT flags = kb.Flags;
+ USHORT vkey = kb.VKey;
+
+ if (makeCode == 0) {
+ if (vkey == VK_SNAPSHOT) {
+ return DIK_SYSRQ;
+ }
+ if (vkey == VK_PAUSE) {
+ return DIK_PAUSE;
+ }
+ return 0;
+ }
+
+ if (vkey == VK_PAUSE) {
+ return DIK_PAUSE;
+ }
+
+ if (vkey == VK_SNAPSHOT) {
+ return DIK_SYSRQ;
+ }
+
+ DWORD dik = makeCode & 0x7F;
+
+ if (flags & RI_KEY_E0) {
+ dik |= 0x80;
+ }
+
+ switch (vkey) {
+ case VK_RCONTROL:
+ return DIK_RCONTROL;
+
+ case VK_RMENU:
+ return DIK_RMENU;
+
+ case VK_LWIN:
+ return DIK_LWIN;
+
+ case VK_RWIN:
+ return DIK_RWIN;
+
+ case VK_APPS:
+ return DIK_APPS;
+
+ default:
+ break;
+ }
+
+ return dik;
+}
+
+enum iceDIShimDeviceType {
+ ICE_DI_DEVICE_KEYBOARD,
+ ICE_DI_DEVICE_MOUSE
+};
+
+static bool IceDI_RegisterRawInputForCurrentTargets(HWND fallbackHwnd) {
+ HWND keyboardTarget = NULL;
+ HWND mouseTarget = NULL;
+
+ {
+ std::lock_guard lock(g_windowMutex);
+
+ if (IceDI_IsUsableWindow(g_keyboardTargetHwnd)) {
+ keyboardTarget = g_keyboardTargetHwnd;
+ }
+
+ if (IceDI_IsUsableWindow(g_mouseTargetHwnd)) {
+ mouseTarget = g_mouseTargetHwnd;
+ }
+
+ if (!IceDI_IsUsableWindow(keyboardTarget) && IceDI_IsUsableWindow(fallbackHwnd)) {
+ keyboardTarget = fallbackHwnd;
+ g_keyboardTargetHwnd = fallbackHwnd;
+ }
+
+ if (!IceDI_IsUsableWindow(mouseTarget) && IceDI_IsUsableWindow(fallbackHwnd)) {
+ mouseTarget = fallbackHwnd;
+ g_mouseTargetHwnd = fallbackHwnd;
+ }
+
+ if (IceDI_IsUsableWindow(fallbackHwnd)) {
+ g_rawInputHwnd = fallbackHwnd;
+ }
+ else {
+ g_rawInputHwnd = IceDI_GetFallbackRawInputWindow_NoLock();
+ }
+ }
+
+ if (!IceDI_IsUsableWindow(keyboardTarget) && !IceDI_IsUsableWindow(mouseTarget)) {
+ return false;
+ }
+
+ RAWINPUTDEVICE rid[2];
+ UINT count = 0;
+ memset(rid, 0, sizeof(rid));
+
+ if (IceDI_IsUsableWindow(keyboardTarget)) {
+ rid[count].usUsagePage = 0x01;
+ rid[count].usUsage = 0x06;
+ rid[count].dwFlags = 0;
+ rid[count].hwndTarget = keyboardTarget;
+ count++;
+ }
+
+ if (IceDI_IsUsableWindow(mouseTarget)) {
+ rid[count].usUsagePage = 0x01;
+ rid[count].usUsage = 0x02;
+ rid[count].dwFlags = 0;
+ rid[count].hwndTarget = mouseTarget;
+ count++;
+ }
+
+ if (count == 0) {
+ return false;
+ }
+
+ if (!::RegisterRawInputDevices(rid, count, sizeof(RAWINPUTDEVICE))) {
+ OutputDebugStringA("IceDirectInputShim: RegisterRawInputDevices failed\n");
+ return false;
+ }
+
+ return true;
+}
+
+static void IceDI_SetDeviceTargetWindow(iceDIShimDeviceType type, HWND hwnd) {
+ if (!IceDI_IsUsableWindow(hwnd)) {
+ return;
+ }
+
+ {
+ std::lock_guard lock(g_windowMutex);
+
+ if (type == ICE_DI_DEVICE_KEYBOARD) {
+ g_keyboardTargetHwnd = hwnd;
+ }
+ else {
+ g_mouseTargetHwnd = hwnd;
+ }
+
+ g_rawInputHwnd = hwnd;
+ }
+
+ IceDI_RegisterRawInputForCurrentTargets(hwnd);
+}
+
+// ------------------------------------------------------------
+// IceDirectInputDevice8
+// ------------------------------------------------------------
+
+class IceDirectInputDevice8 : public IDirectInputDevice8A {
+public:
+ IceDirectInputDevice8(iceDIShimDeviceType type) :
+ refCount(1),
+ deviceType(type),
+ acquired(false),
+ bufferSize(ICE_DI_DEFAULT_BUFFER_SIZE),
+ coopFlags(0),
+ hwnd(NULL),
+ haveLastAbsoluteMouse(false) {
+
+ memset(keyboardState, 0, sizeof(keyboardState));
+ memset(&mouseState, 0, sizeof(mouseState));
+ lastAbsoluteMouse.x = 0;
+ lastAbsoluteMouse.y = 0;
+ }
+
+ virtual ~IceDirectInputDevice8() {
+ std::lock_guard lock(g_deviceMutex);
+
+ if (g_keyboardDevice == this) {
+ g_keyboardDevice = NULL;
+ }
+
+ if (g_mouseDevice == this) {
+ g_mouseDevice = NULL;
+ g_iceDI_MouseAcquired.store(false);
+ }
+ }
+
+ // --------------------------------------------------------
+ // IUnknown
+ // --------------------------------------------------------
+
+ STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) override {
+ if (ppvObj == NULL) {
+ return E_POINTER;
+ }
+
+ *ppvObj = NULL;
+
+ if (riid == IID_IUnknown || riid == IID_IDirectInputDevice8A) {
+ *ppvObj = static_cast(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHOD_(ULONG, AddRef)() override {
+ return ++refCount;
+ }
+
+ STDMETHOD_(ULONG, Release)() override {
+ ULONG r = --refCount;
+
+ if (r == 0) {
+ delete this;
+ }
+
+ return r;
+ }
+
+ // --------------------------------------------------------
+ // IDirectInputDevice8A
+ // --------------------------------------------------------
+
+ STDMETHOD(GetCapabilities)(LPDIDEVCAPS lpDIDevCaps) override {
+ if (lpDIDevCaps == NULL) {
+ return E_POINTER;
+ }
+
+ if (lpDIDevCaps->dwSize < sizeof(DIDEVCAPS)) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ DWORD oldSize = lpDIDevCaps->dwSize;
+ memset(lpDIDevCaps, 0, oldSize);
+ lpDIDevCaps->dwSize = oldSize;
+
+ if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
+ lpDIDevCaps->dwDevType = DI8DEVTYPE_KEYBOARD;
+ lpDIDevCaps->dwButtons = 256;
+ }
+ else {
+ lpDIDevCaps->dwDevType = DI8DEVTYPE_MOUSE;
+ lpDIDevCaps->dwAxes = 3;
+ lpDIDevCaps->dwButtons = 8;
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(EnumObjects)(LPDIENUMDEVICEOBJECTSCALLBACKA lpCallback, LPVOID pvRef, DWORD dwFlags) override {
+ if (lpCallback == NULL) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
+ for (DWORD i = 0; i < 256; i++) {
+ DIDEVICEOBJECTINSTANCEA inst;
+ memset(&inst, 0, sizeof(inst));
+
+ inst.dwSize = sizeof(inst);
+ inst.dwType = DIDFT_BUTTON | DIDFT_MAKEINSTANCE(i);
+ inst.dwOfs = i;
+ sprintf_s(inst.tszName, "Key %u", i);
+
+ if (lpCallback(&inst, pvRef) == DIENUM_STOP) {
+ break;
+ }
+ }
+ }
+ else {
+ static const DWORD mouseObjects[] = {
+ DIMOFS_X,
+ DIMOFS_Y,
+ DIMOFS_Z,
+ DIMOFS_BUTTON0,
+ DIMOFS_BUTTON1,
+ DIMOFS_BUTTON2,
+ DIMOFS_BUTTON3,
+ DIMOFS_BUTTON4,
+ DIMOFS_BUTTON5,
+ DIMOFS_BUTTON6,
+ DIMOFS_BUTTON7
+ };
+
+ for (int i = 0; i < (int)(sizeof(mouseObjects) / sizeof(mouseObjects[0])); i++) {
+ DIDEVICEOBJECTINSTANCEA inst;
+ memset(&inst, 0, sizeof(inst));
+
+ inst.dwSize = sizeof(inst);
+ inst.dwOfs = mouseObjects[i];
+
+ if (mouseObjects[i] == DIMOFS_X || mouseObjects[i] == DIMOFS_Y || mouseObjects[i] == DIMOFS_Z) {
+ inst.dwType = DIDFT_AXIS | DIDFT_MAKEINSTANCE(i);
+ }
+ else {
+ inst.dwType = DIDFT_BUTTON | DIDFT_MAKEINSTANCE(i - 3);
+ }
+
+ sprintf_s(inst.tszName, "Mouse Object %d", i);
+
+ if (lpCallback(&inst, pvRef) == DIENUM_STOP) {
+ break;
+ }
+ }
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(GetProperty)(REFGUID rguidProp, LPDIPROPHEADER pdiph) override {
+ if (pdiph == NULL) {
+ return E_POINTER;
+ }
+
+ if (IceDI_IsDInputProp(rguidProp, 1)) { // DIPROP_BUFFERSIZE
+ if (pdiph->dwSize < sizeof(DIPROPDWORD)) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ DIPROPDWORD* prop = reinterpret_cast(pdiph);
+ prop->dwData = bufferSize;
+ return DI_OK;
+ }
+
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(SetProperty)(REFGUID rguidProp, LPCDIPROPHEADER pdiph) override {
+ if (pdiph == NULL) {
+ return E_POINTER;
+ }
+
+ if (IceDI_IsDInputProp(rguidProp, 1)) { // DIPROP_BUFFERSIZE
+ if (pdiph->dwSize < sizeof(DIPROPDWORD)) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ const DIPROPDWORD* prop = reinterpret_cast(pdiph);
+
+ std::lock_guard lock(mutex);
+ bufferSize = prop->dwData > 0 ? prop->dwData : ICE_DI_DEFAULT_BUFFER_SIZE;
+ TrimQueue_NoLock();
+
+ return DI_OK;
+ }
+
+ // Ignore unsupported DirectInput properties instead of failing old code.
+ return DI_OK;
+ }
+
+ STDMETHOD(Acquire)() override {
+ acquired = true;
+
+ if (hwnd != NULL && ::IsWindow(hwnd)) {
+ IceDI_SetDeviceTargetWindow(deviceType, hwnd);
+ }
+ else {
+ HWND fallback = NULL;
+ {
+ std::lock_guard lock(g_windowMutex);
+ fallback = IceDI_GetFallbackRawInputWindow_NoLock();
+ }
+
+ if (fallback != NULL && ::IsWindow(fallback)) {
+ IceDI_RegisterRawInputForCurrentTargets(fallback);
+ }
+ }
+
+ if (deviceType == ICE_DI_DEVICE_MOUSE) {
+ g_iceDI_MouseAcquired.store(true);
+ ResetMouseMotion();
+ IceDI_ForceMouseCenterInternal(true);
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(Unacquire)() override {
+ acquired = false;
+
+ if (deviceType == ICE_DI_DEVICE_MOUSE) {
+ g_iceDI_MouseAcquired.store(false);
+ ResetMouseMotion();
+ IceDI_ClearMouseClipIfOwned();
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(GetDeviceState)(DWORD cbData, LPVOID lpvData) override {
+ if (lpvData == NULL) {
+ return E_POINTER;
+ }
+
+ if (!acquired) {
+ return DIERR_NOTACQUIRED;
+ }
+
+ if (deviceType == ICE_DI_DEVICE_MOUSE) {
+ IceDI_ForceMouseCenterInternal(false);
+ }
+
+ std::lock_guard lock(mutex);
+
+ if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
+ if (cbData < 256) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ memcpy(lpvData, keyboardState, 256);
+ return DI_OK;
+ }
+
+ if (cbData >= sizeof(DIMOUSESTATE2)) {
+ memcpy(lpvData, &mouseState, sizeof(DIMOUSESTATE2));
+
+ // Relative mouse state is consumed per poll.
+ mouseState.lX = 0;
+ mouseState.lY = 0;
+ mouseState.lZ = 0;
+
+ return DI_OK;
+ }
+
+ if (cbData >= sizeof(DIMOUSESTATE)) {
+ DIMOUSESTATE out;
+ memset(&out, 0, sizeof(out));
+
+ out.lX = mouseState.lX;
+ out.lY = mouseState.lY;
+ out.lZ = mouseState.lZ;
+ memcpy(out.rgbButtons, mouseState.rgbButtons, 4);
+
+ memcpy(lpvData, &out, sizeof(out));
+
+ mouseState.lX = 0;
+ mouseState.lY = 0;
+ mouseState.lZ = 0;
+
+ return DI_OK;
+ }
+
+ return DIERR_INVALIDPARAM;
+ }
+
+ STDMETHOD(GetDeviceData)(
+ DWORD cbObjectData,
+ LPDIDEVICEOBJECTDATA rgdod,
+ LPDWORD pdwInOut,
+ DWORD dwFlags
+ ) override {
+ if (pdwInOut == NULL) {
+ return E_POINTER;
+ }
+
+ if (cbObjectData < sizeof(DIDEVICEOBJECTDATA)) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ if (!acquired) {
+ return DIERR_NOTACQUIRED;
+ }
+
+ if (deviceType == ICE_DI_DEVICE_MOUSE) {
+ IceDI_ForceMouseCenterInternal(false);
+ }
+
+ std::lock_guard lock(mutex);
+
+ DWORD requested = *pdwInOut;
+
+ // Doom/idTech uses rgdod == NULL to clear buffered garbage.
+ if (rgdod == NULL) {
+ DWORD count = 0;
+
+ if (requested == 0) {
+ count = (DWORD)queue.size();
+ queue.clear();
+ }
+ else {
+ count = std::min(requested, (DWORD)queue.size());
+
+ for (DWORD i = 0; i < count; i++) {
+ queue.pop_front();
+ }
+ }
+
+ *pdwInOut = count;
+ return DI_OK;
+ }
+
+ DWORD count = std::min(requested, (DWORD)queue.size());
+
+ for (DWORD i = 0; i < count; i++) {
+ rgdod[i] = queue.front();
+ queue.pop_front();
+ }
+
+ *pdwInOut = count;
+ return DI_OK;
+ }
+
+ STDMETHOD(SetDataFormat)(LPCDIDATAFORMAT lpdf) override {
+ if (lpdf == NULL) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ // Existing Doom path passes c_dfDIKeyboard / c_dfDIMouse2.
+ // The shim always emits standard DirectInput offsets.
+ return DI_OK;
+ }
+
+ STDMETHOD(SetEventNotification)(HANDLE hEvent) override {
+ return DI_OK;
+ }
+
+ STDMETHOD(SetCooperativeLevel)(HWND hwndOwner, DWORD dwFlags) override {
+ hwnd = hwndOwner;
+ coopFlags = dwFlags;
+
+ if (hwndOwner != NULL && ::IsWindow(hwndOwner)) {
+ IceDI_SetDeviceTargetWindow(deviceType, hwndOwner);
+
+ if (deviceType == ICE_DI_DEVICE_MOUSE) {
+ IceDI_ForceMouseCenterInternal(true);
+ }
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(GetObjectInfo)(LPDIDEVICEOBJECTINSTANCEA pdidoi, DWORD dwObj, DWORD dwHow) override {
+ if (pdidoi == NULL) {
+ return E_POINTER;
+ }
+
+ DWORD oldSize = pdidoi->dwSize;
+ memset(pdidoi, 0, oldSize);
+ pdidoi->dwSize = oldSize;
+ pdidoi->dwOfs = dwObj;
+
+ sprintf_s(pdidoi->tszName, "Ice RawInput Object %u", dwObj);
+
+ return DI_OK;
+ }
+
+ STDMETHOD(GetDeviceInfo)(LPDIDEVICEINSTANCEA pdidi) override {
+ if (pdidi == NULL) {
+ return E_POINTER;
+ }
+
+ DWORD oldSize = pdidi->dwSize;
+ memset(pdidi, 0, oldSize);
+ pdidi->dwSize = oldSize;
+
+ if (deviceType == ICE_DI_DEVICE_KEYBOARD) {
+ pdidi->guidInstance = GUID_SysKeyboard;
+ pdidi->guidProduct = GUID_SysKeyboard;
+ pdidi->dwDevType = DI8DEVTYPE_KEYBOARD;
+
+ strcpy_s(pdidi->tszInstanceName, "Ice RawInput Keyboard");
+ strcpy_s(pdidi->tszProductName, "Ice RawInput Keyboard");
+ }
+ else {
+ pdidi->guidInstance = GUID_SysMouse;
+ pdidi->guidProduct = GUID_SysMouse;
+ pdidi->dwDevType = DI8DEVTYPE_MOUSE;
+
+ strcpy_s(pdidi->tszInstanceName, "Ice RawInput Mouse");
+ strcpy_s(pdidi->tszProductName, "Ice RawInput Mouse");
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(RunControlPanel)(HWND hwndOwner, DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(Initialize)(HINSTANCE hinst, DWORD dwVersion, REFGUID rguid) override {
+ return DI_OK;
+ }
+
+ STDMETHOD(CreateEffect)(REFGUID rguid, LPCDIEFFECT lpeff, LPDIRECTINPUTEFFECT* ppdeff, LPUNKNOWN punkOuter) override {
+ if (ppdeff != NULL) {
+ *ppdeff = NULL;
+ }
+
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(EnumEffects)(LPDIENUMEFFECTSCALLBACKA lpCallback, LPVOID pvRef, DWORD dwEffType) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(GetEffectInfo)(LPDIEFFECTINFOA pdei, REFGUID rguid) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(GetForceFeedbackState)(LPDWORD pdwOut) override {
+ if (pdwOut != NULL) {
+ *pdwOut = 0;
+ }
+
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(SendForceFeedbackCommand)(DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(EnumCreatedEffectObjects)(LPDIENUMCREATEDEFFECTOBJECTSCALLBACK lpCallback, LPVOID pvRef, DWORD fl) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(Escape)(LPDIEFFESCAPE pesc) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(Poll)() override {
+ if (deviceType == ICE_DI_DEVICE_MOUSE && acquired) {
+ IceDI_ForceMouseCenterInternal(false);
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(SendDeviceData)(DWORD cbObjectData, LPCDIDEVICEOBJECTDATA rgdod, LPDWORD pdwInOut, DWORD fl) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(EnumEffectsInFile)(LPCSTR lpszFileName, LPDIENUMEFFECTSINFILECALLBACK pec, LPVOID pvRef, DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(WriteEffectToFile)(LPCSTR lpszFileName, DWORD dwEntries, LPDIFILEEFFECT rgDiFileEft, DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(BuildActionMap)(LPDIACTIONFORMATA lpdiaf, LPCSTR lpszUserName, DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(SetActionMap)(LPDIACTIONFORMATA lpdiActionFormat, LPCSTR lptszUserName, DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(GetImageInfo)(LPDIDEVICEIMAGEINFOHEADERA lpdiDevImageInfoHeader) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ // --------------------------------------------------------
+ // Raw Input entry points
+ // --------------------------------------------------------
+
+ void PushKeyboardRaw(const RAWKEYBOARD& kb) {
+ DWORD dik = IceDI_RawKeyboardToDIK(kb);
+
+ if (dik == 0 || dik >= 256) {
+ return;
+ }
+
+ const bool down = IceDI_IsPressedRawKeyboard(kb);
+ const BYTE newState = down ? 0x80 : 0x00;
+
+ std::lock_guard lock(mutex);
+
+ keyboardState[dik] = newState;
+
+ DIDEVICEOBJECTDATA od;
+ memset(&od, 0, sizeof(od));
+
+ od.dwOfs = dik;
+ od.dwData = newState;
+ od.dwTimeStamp = IceDI_GetTimeStamp();
+ od.dwSequence = 0;
+ od.uAppData = 0;
+
+ PushEvent_NoLock(od);
+ }
+
+ void PushMouseRaw(const RAWMOUSE& rm) {
+ std::lock_guard lock(mutex);
+
+ const DWORD time = IceDI_GetTimeStamp();
+ LONG dx = 0;
+ LONG dy = 0;
+
+ if (rm.usFlags & MOUSE_MOVE_ABSOLUTE) {
+ POINT absolutePoint;
+ if (IceDI_RawAbsoluteMouseToScreen(rm, &absolutePoint)) {
+ // SetCursorPos can surface as an absolute packet on some mouse
+ // stacks / remote-desktop / wrapped-window setups. Reset the
+ // baseline instead of generating a giant negative/positive delta.
+ if (IceDI_IsRecentCenterWarpPoint(absolutePoint)) {
+ lastAbsoluteMouse = absolutePoint;
+ haveLastAbsoluteMouse = true;
+ }
+ else if (haveLastAbsoluteMouse) {
+ dx = absolutePoint.x - lastAbsoluteMouse.x;
+ dy = absolutePoint.y - lastAbsoluteMouse.y;
+ lastAbsoluteMouse = absolutePoint;
+ }
+ else {
+ lastAbsoluteMouse = absolutePoint;
+ haveLastAbsoluteMouse = true;
+ }
+ }
+ }
+ else {
+ haveLastAbsoluteMouse = false;
+ dx = rm.lLastX;
+ dy = rm.lLastY;
+ }
+
+ // Raw Input relative deltas only. The absolute packet conversion above
+ // converts absolute devices into sane relative deltas; do not mix this
+ // with GetCursorPos/WM_MOUSEMOVE deltas.
+ if (dx != 0) {
+ mouseState.lX += dx;
+ PushMouseAxis_NoLock(DIMOFS_X, dx, time);
+ }
+
+ if (dy != 0) {
+ mouseState.lY += dy;
+ PushMouseAxis_NoLock(DIMOFS_Y, dy, time);
+ }
+
+ const USHORT flags = rm.usButtonFlags;
+
+ if (flags & RI_MOUSE_WHEEL) {
+ SHORT wheel = (SHORT)rm.usButtonData;
+ mouseState.lZ += wheel;
+ PushMouseAxis_NoLock(DIMOFS_Z, wheel, time);
+ }
+
+ if (flags & RI_MOUSE_BUTTON_1_DOWN) {
+ PushMouseButton_NoLock(0, true, time);
+ }
+ if (flags & RI_MOUSE_BUTTON_1_UP) {
+ PushMouseButton_NoLock(0, false, time);
+ }
+
+ if (flags & RI_MOUSE_BUTTON_2_DOWN) {
+ PushMouseButton_NoLock(1, true, time);
+ }
+ if (flags & RI_MOUSE_BUTTON_2_UP) {
+ PushMouseButton_NoLock(1, false, time);
+ }
+
+ if (flags & RI_MOUSE_BUTTON_3_DOWN) {
+ PushMouseButton_NoLock(2, true, time);
+ }
+ if (flags & RI_MOUSE_BUTTON_3_UP) {
+ PushMouseButton_NoLock(2, false, time);
+ }
+
+ if (flags & RI_MOUSE_BUTTON_4_DOWN) {
+ PushMouseButton_NoLock(3, true, time);
+ }
+ if (flags & RI_MOUSE_BUTTON_4_UP) {
+ PushMouseButton_NoLock(3, false, time);
+ }
+
+ if (flags & RI_MOUSE_BUTTON_5_DOWN) {
+ PushMouseButton_NoLock(4, true, time);
+ }
+ if (flags & RI_MOUSE_BUTTON_5_UP) {
+ PushMouseButton_NoLock(4, false, time);
+ }
+ }
+
+ void ClearQueue() {
+ std::lock_guard lock(mutex);
+ queue.clear();
+ }
+
+ void ResetMouseMotion() {
+ if (deviceType != ICE_DI_DEVICE_MOUSE) {
+ return;
+ }
+
+ std::lock_guard lock(mutex);
+ queue.clear();
+ memset(&mouseState, 0, sizeof(mouseState));
+ haveLastAbsoluteMouse = false;
+ lastAbsoluteMouse.x = 0;
+ lastAbsoluteMouse.y = 0;
+ }
+
+private:
+ void PushMouseAxis_NoLock(DWORD ofs, LONG value, DWORD time) {
+ DIDEVICEOBJECTDATA od;
+ memset(&od, 0, sizeof(od));
+
+ od.dwOfs = ofs;
+ od.dwData = (DWORD)value;
+ od.dwTimeStamp = time;
+ od.dwSequence = 0;
+ od.uAppData = 0;
+
+ PushEvent_NoLock(od);
+ }
+
+ void PushMouseButton_NoLock(int button, bool down, DWORD time) {
+ if (button < 0 || button >= 8) {
+ return;
+ }
+
+ mouseState.rgbButtons[button] = down ? 0x80 : 0x00;
+
+ DIDEVICEOBJECTDATA od;
+ memset(&od, 0, sizeof(od));
+
+ od.dwOfs = DIMOFS_BUTTON0 + button;
+ od.dwData = down ? 0x80 : 0x00;
+ od.dwTimeStamp = time;
+ od.dwSequence = 0;
+ od.uAppData = 0;
+
+ PushEvent_NoLock(od);
+ }
+
+ void PushEvent_NoLock(const DIDEVICEOBJECTDATA& od) {
+ queue.push_back(od);
+ TrimQueue_NoLock();
+ }
+
+ void TrimQueue_NoLock() {
+ while (queue.size() > bufferSize) {
+ queue.pop_front();
+ }
+ }
+
+private:
+ std::atomic refCount;
+ iceDIShimDeviceType deviceType;
+ bool acquired;
+ DWORD bufferSize;
+ DWORD coopFlags;
+ HWND hwnd;
+
+ std::mutex mutex;
+ std::deque queue;
+
+ BYTE keyboardState[256];
+ DIMOUSESTATE2 mouseState;
+
+ bool haveLastAbsoluteMouse;
+ POINT lastAbsoluteMouse;
+};
+
+// ------------------------------------------------------------
+// IceDirectInput8
+// ------------------------------------------------------------
+
+class IceDirectInput8 : public IDirectInput8A {
+public:
+ IceDirectInput8() : refCount(1) {
+ }
+
+ virtual ~IceDirectInput8() {
+ }
+
+ STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppvObj) override {
+ if (ppvObj == NULL) {
+ return E_POINTER;
+ }
+
+ *ppvObj = NULL;
+
+ if (riid == IID_IUnknown || riid == IID_IDirectInput8A) {
+ *ppvObj = static_cast(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ STDMETHOD_(ULONG, AddRef)() override {
+ return ++refCount;
+ }
+
+ STDMETHOD_(ULONG, Release)() override {
+ ULONG r = --refCount;
+
+ if (r == 0) {
+ delete this;
+ }
+
+ return r;
+ }
+
+ STDMETHOD(CreateDevice)(REFGUID rguid, LPDIRECTINPUTDEVICE8A* lplpDirectInputDevice, LPUNKNOWN pUnkOuter) override {
+ if (lplpDirectInputDevice == NULL) {
+ return E_POINTER;
+ }
+
+ *lplpDirectInputDevice = NULL;
+
+ if (pUnkOuter != NULL) {
+ return CLASS_E_NOAGGREGATION;
+ }
+
+ IceDirectInputDevice8* dev = NULL;
+ IceDirectInputDevice8* oldDev = NULL;
+
+ if (rguid == GUID_SysKeyboard) {
+ dev = new IceDirectInputDevice8(ICE_DI_DEVICE_KEYBOARD);
+
+ {
+ std::lock_guard lock(g_deviceMutex);
+ oldDev = g_keyboardDevice;
+ g_keyboardDevice = dev;
+ g_keyboardDevice->AddRef();
+ }
+ }
+ else if (rguid == GUID_SysMouse) {
+ dev = new IceDirectInputDevice8(ICE_DI_DEVICE_MOUSE);
+
+ {
+ std::lock_guard lock(g_deviceMutex);
+ oldDev = g_mouseDevice;
+ g_mouseDevice = dev;
+ g_mouseDevice->AddRef();
+ }
+ }
+ else {
+ return DIERR_DEVICENOTREG;
+ }
+
+ // Do not Release while holding g_deviceMutex. The device destructor also
+ // takes g_deviceMutex to clear the globals.
+ if (oldDev != NULL) {
+ oldDev->Release();
+ }
+
+ *lplpDirectInputDevice = dev;
+ return DI_OK;
+ }
+
+ STDMETHOD(EnumDevices)(DWORD dwDevType, LPDIENUMDEVICESCALLBACKA lpCallback, LPVOID pvRef, DWORD dwFlags) override {
+ if (lpCallback == NULL) {
+ return DIERR_INVALIDPARAM;
+ }
+
+ if (dwDevType == 0 || dwDevType == DI8DEVCLASS_KEYBOARD || dwDevType == DI8DEVTYPE_KEYBOARD) {
+ DIDEVICEINSTANCEA di;
+ memset(&di, 0, sizeof(di));
+
+ di.dwSize = sizeof(di);
+ di.guidInstance = GUID_SysKeyboard;
+ di.guidProduct = GUID_SysKeyboard;
+ di.dwDevType = DI8DEVTYPE_KEYBOARD;
+
+ strcpy_s(di.tszInstanceName, "Ice RawInput Keyboard");
+ strcpy_s(di.tszProductName, "Ice RawInput Keyboard");
+
+ if (lpCallback(&di, pvRef) == DIENUM_STOP) {
+ return DI_OK;
+ }
+ }
+
+ if (dwDevType == 0 || dwDevType == DI8DEVCLASS_POINTER || dwDevType == DI8DEVTYPE_MOUSE) {
+ DIDEVICEINSTANCEA di;
+ memset(&di, 0, sizeof(di));
+
+ di.dwSize = sizeof(di);
+ di.guidInstance = GUID_SysMouse;
+ di.guidProduct = GUID_SysMouse;
+ di.dwDevType = DI8DEVTYPE_MOUSE;
+
+ strcpy_s(di.tszInstanceName, "Ice RawInput Mouse");
+ strcpy_s(di.tszProductName, "Ice RawInput Mouse");
+
+ lpCallback(&di, pvRef);
+ }
+
+ return DI_OK;
+ }
+
+ STDMETHOD(GetDeviceStatus)(REFGUID rguidInstance) override {
+ if (rguidInstance == GUID_SysKeyboard || rguidInstance == GUID_SysMouse) {
+ return DI_OK;
+ }
+
+ return DIERR_NOTFOUND;
+ }
+
+ STDMETHOD(RunControlPanel)(HWND hwndOwner, DWORD dwFlags) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(Initialize)(HINSTANCE hinst, DWORD dwVersion) override {
+ return DI_OK;
+ }
+
+ STDMETHOD(FindDevice)(REFGUID rguidClass, LPCSTR ptszName, LPGUID pguidInstance) override {
+ if (pguidInstance == NULL) {
+ return E_POINTER;
+ }
+
+ if (ptszName != NULL) {
+ if (_stricmp(ptszName, "keyboard") == 0) {
+ *pguidInstance = GUID_SysKeyboard;
+ return DI_OK;
+ }
+
+ if (_stricmp(ptszName, "mouse") == 0) {
+ *pguidInstance = GUID_SysMouse;
+ return DI_OK;
+ }
+ }
+
+ return DIERR_NOTFOUND;
+ }
+
+ STDMETHOD(EnumDevicesBySemantics)(
+ LPCSTR ptszUserName,
+ LPDIACTIONFORMATA lpdiActionFormat,
+ LPDIENUMDEVICESBYSEMANTICSCBA lpCallback,
+ LPVOID pvRef,
+ DWORD dwFlags
+ ) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+ STDMETHOD(ConfigureDevices)(
+ LPDICONFIGUREDEVICESCALLBACK lpdiCallback,
+ LPDICONFIGUREDEVICESPARAMSA lpdiCDParams,
+ DWORD dwFlags,
+ LPVOID pvRefData
+ ) override {
+ return DIERR_UNSUPPORTED;
+ }
+
+private:
+ std::atomic refCount;
+};
+
+// ------------------------------------------------------------
+// Public API
+// ------------------------------------------------------------
+
+HRESULT WINAPI IceDirectInput8Create(
+ HINSTANCE hinst,
+ DWORD dwVersion,
+ REFIID riidltf,
+ LPVOID* ppvOut,
+ LPUNKNOWN punkOuter
+) {
+ if (ppvOut == NULL) {
+ return E_POINTER;
+ }
+
+ *ppvOut = NULL;
+
+ if (punkOuter != NULL) {
+ return CLASS_E_NOAGGREGATION;
+ }
+
+ IceDirectInput8* di = new IceDirectInput8();
+
+ HRESULT hr = di->QueryInterface(riidltf, ppvOut);
+ di->Release();
+
+ return hr;
+}
+
+bool IceDirectInputShim_RegisterRawInput(HWND hwnd) {
+ if (hwnd == NULL || !::IsWindow(hwnd)) {
+ return false;
+ }
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ g_rawInputHwnd = hwnd;
+ g_keyboardTargetHwnd = hwnd;
+ g_mouseTargetHwnd = hwnd;
+ }
+
+ bool ok = IceDI_RegisterRawInputForCurrentTargets(hwnd);
+
+ if (ok) {
+ IceDI_ForceMouseCenterInternal(false);
+ }
+
+ return ok;
+}
+
+void IceDirectInputShim_SetMouseTargetWindow(HWND hwnd) {
+ if (hwnd == NULL || !::IsWindow(hwnd)) {
+ return;
+ }
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ g_mouseTargetHwnd = hwnd;
+ g_rawInputHwnd = hwnd;
+ }
+
+ IceDI_RegisterRawInputForCurrentTargets(hwnd);
+ IceDI_ForceMouseCenterInternal(true);
+}
+
+void IceDirectInputShim_SetMouseCenterLock(bool enabled) {
+ g_iceDI_CenterMouseWhenActive.store(enabled);
+
+ if (!enabled) {
+ IceDI_ClearMouseClipIfOwned();
+ }
+ else {
+ IceDI_ForceMouseCenterInternal(true);
+ }
+}
+
+bool IceDirectInputShim_ForceMouseCenter() {
+ return IceDI_ForceMouseCenterInternal(true);
+}
+
+bool IceDirectInputShim_HandleRawInput(LPARAM lParam) {
+ UINT size = 0;
+
+ if (::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)) != 0) {
+ return false;
+ }
+
+ if (size == 0) {
+ return false;
+ }
+
+ std::vector buffer;
+ buffer.resize(size);
+
+ UINT result = ::GetRawInputData(
+ (HRAWINPUT)lParam,
+ RID_INPUT,
+ buffer.data(),
+ &size,
+ sizeof(RAWINPUTHEADER)
+ );
+
+ if (result == (UINT)-1 || result == 0) {
+ return false;
+ }
+
+ RAWINPUT* raw = reinterpret_cast(buffer.data());
+
+ // RAWINPUTHEADER intentionally has no target HWND field. The target window
+ // is chosen when registering RAWINPUTDEVICE::hwndTarget, in SetCooperativeLevel,
+ // or through IceDirectInputShim_SetMouseTargetWindow(). Do not try to read
+ // the target HWND from the raw-input header; that member does not exist.
+
+ if (raw->header.dwType == RIM_TYPEKEYBOARD) {
+ IceDirectInputDevice8* kbd = NULL;
+
+ {
+ std::lock_guard lock(g_deviceMutex);
+ kbd = g_keyboardDevice;
+
+ if (kbd != NULL) {
+ kbd->AddRef();
+ }
+ }
+
+ if (kbd != NULL) {
+ kbd->PushKeyboardRaw(raw->data.keyboard);
+ kbd->Release();
+ return true;
+ }
+ }
+
+ if (raw->header.dwType == RIM_TYPEMOUSE) {
+ if (!g_iceDI_GameMouseActive.load()) {
+ IceDI_ClearMouseClipIfOwned();
+ return false;
+ }
+
+ IceDirectInputDevice8* mouse = NULL;
+
+ {
+ std::lock_guard lock(g_deviceMutex);
+ mouse = g_mouseDevice;
+
+ if (mouse != NULL) {
+ mouse->AddRef();
+ }
+ }
+
+ if (mouse != NULL) {
+ mouse->PushMouseRaw(raw->data.mouse);
+ mouse->Release();
+ IceDI_ForceMouseCenterInternal(false);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void IceDirectInputShim_SetGameMouseActive(bool active) {
+ g_iceDI_GameMouseActive.store(active);
+
+ IceDirectInputDevice8* mouse = NULL;
+
+ {
+ std::lock_guard lock(g_deviceMutex);
+ mouse = g_mouseDevice;
+
+ if (mouse != NULL) {
+ mouse->AddRef();
+ }
+ }
+
+ if (mouse != NULL) {
+ mouse->ResetMouseMotion();
+ mouse->Release();
+ }
+
+ if (active) {
+ IceDI_ForceMouseCenterInternal(true);
+ }
+ else {
+ IceDI_ClearMouseClipIfOwned();
+ }
+}
+
+void IceDirectInputShim_Shutdown() {
+ IceDirectInputDevice8* keyboard = NULL;
+ IceDirectInputDevice8* mouse = NULL;
+
+ {
+ std::lock_guard lock(g_deviceMutex);
+ keyboard = g_keyboardDevice;
+ mouse = g_mouseDevice;
+ g_keyboardDevice = NULL;
+ g_mouseDevice = NULL;
+ }
+
+ if (keyboard != NULL) {
+ keyboard->Release();
+ }
+
+ if (mouse != NULL) {
+ mouse->Release();
+ }
+
+ {
+ std::lock_guard lock(g_windowMutex);
+ g_rawInputHwnd = NULL;
+ g_keyboardTargetHwnd = NULL;
+ g_mouseTargetHwnd = NULL;
+ g_lastCenterWarpTick = 0;
+ }
+
+ g_iceDI_MouseAcquired.store(false);
+ g_iceDI_GameMouseActive.store(false);
+ IceDI_ClearMouseClipIfOwned();
+}
diff --git a/neo/engine/opengl/DirectInputShim.h b/neo/engine/opengl/DirectInputShim.h
new file mode 100644
index 00000000..0690c1d0
--- /dev/null
+++ b/neo/engine/opengl/DirectInputShim.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include
+#include
+
+#ifndef DIRECTINPUT_VERSION
+#define DIRECTINPUT_VERSION 0x0800
+#endif
+
+// Call this instead of DirectInput8Create.
+HRESULT WINAPI IceDirectInput8Create(
+ HINSTANCE hinst,
+ DWORD dwVersion,
+ REFIID riidltf,
+ LPVOID* ppvOut,
+ LPUNKNOWN punkOuter
+);
+
+// Call from your main window proc on WM_INPUT.
+bool IceDirectInputShim_HandleRawInput(LPARAM lParam);
+
+// Optional: call when your hwnd changes/recreates.
+bool IceDirectInputShim_RegisterRawInput(HWND hwnd);
+
+// Optional shutdown helper.
+void IceDirectInputShim_Shutdown();
+
+void IceDI_ForceOverrideSkipCenter(bool shouldSkip);
\ No newline at end of file
diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp
index a338ceb4..c08fd0cd 100644
--- a/neo/engine/opengl/gl_d3d12shim.cpp
+++ b/neo/engine/opengl/gl_d3d12shim.cpp
@@ -188,6 +188,12 @@ using Microsoft::WRL::ComPtr;
#ifndef GL_QD3D12_TONEMAP_BRIGHTNESS
#define GL_QD3D12_TONEMAP_BRIGHTNESS 0x600A
#endif
+#ifndef GL_QD3D12_TAA_ENABLED
+#define GL_QD3D12_TAA_ENABLED 0x600B
+#endif
+#ifndef GL_QD3D12_TAA
+#define GL_QD3D12_TAA GL_QD3D12_TAA_ENABLED
+#endif
#ifndef GL_QD3D12_TONE_MAP_BRIGHTNESS
#define GL_QD3D12_TONE_MAP_BRIGHTNESS GL_QD3D12_TONEMAP_BRIGHTNESS
#endif
@@ -314,6 +320,12 @@ void QD3D12_EnableDLAA(int enabled);
int QD3D12_IsDLAAEnabled(void);
void APIENTRY glDLAAQD3D12(GLboolean enable);
void APIENTRY glEnableDLAAQD3D12(GLboolean enable);
+void QD3D12_EnableTAA(int enabled);
+int QD3D12_IsTAAEnabled(void);
+void APIENTRY glTAAQD3D12(GLboolean enable);
+void APIENTRY glEnableTAAQD3D12(GLboolean enable);
+void APIENTRY glTemporalAAQD3D12(GLboolean enable);
+void APIENTRY glEnableTemporalAAQD3D12(GLboolean enable);
void QD3D12_SetPathTracingQuality(uint32_t samplesPerPixel, uint32_t maxBounces);
void QD3D12_SetPathTracingFallbackSamples(uint32_t samplesPerPixel);
void QD3D12_SetCameraInfo(
@@ -661,7 +673,7 @@ struct GLBufferObject
const char* vendor = "Justin Marshall";
const char* renderer = "Quake D3D12 Wrapper";
const char* version = "1.1-quake-d3d12";
-const char* extensions = "GL_SGIS_multitexture GL_ARB_multitexture GL_EXT_texture_env_add GL_ARB_texture_env_combine GL_ARB_texture_compression GL_EXT_texture_compression_s3tc GL_ARB_vertex_program GL_ARB_fragment_program GL_EXT_texture_cube_map GL_EXT_depth_bounds_test GL_EXT_stencil_two_side GL_ATI_separate_stencil GL_QD3D12_normal_map GL_QD3D12_glow_map GL_QD3D12_specular_map GL_QD3D12_glass_material GL_QD3D12_volumetric_light GL_QD3D12_dlaa GL_QD3D12_tonemap_brightness";
+const char* extensions = "GL_SGIS_multitexture GL_ARB_multitexture GL_EXT_texture_env_add GL_ARB_texture_env_combine GL_ARB_texture_compression GL_EXT_texture_compression_s3tc GL_ARB_vertex_program GL_ARB_fragment_program GL_EXT_texture_cube_map GL_EXT_depth_bounds_test GL_EXT_stencil_two_side GL_ATI_separate_stencil GL_QD3D12_normal_map GL_QD3D12_glow_map GL_QD3D12_specular_map GL_QD3D12_glass_material GL_QD3D12_volumetric_light GL_QD3D12_dlaa GL_QD3D12_taa GL_QD3D12_tonemap_brightness";
enum TexEnvModeShader
{
@@ -1207,6 +1219,7 @@ struct GLState
QD3D12UpscalerBackend upscalerBackend = QD3D12_UPSCALER_DLSS;
QD3D12UpscalerQuality upscalerQuality = QD3D12_QUALITY_PERFORMANCE;
+ bool enableInternalTAA = true;
bool enableRayAIDenoise = false;
bool enableDLSSRayReconstruction = true;
bool enableFSRRayRegeneration = false;
@@ -5874,7 +5887,9 @@ static bool QD3D12_ShouldRunInternalTAA(const QD3D12Window& w)
{
// DLSS/DLSS-RR/FSR are already temporal. The internal TAA pass replaces MSAA
// for the native/no-upscaler path and runs after lighting but before 2D.
- return !w.isPbuffer && !QD3D12_ExternalTemporalUpscalerActive(w);
+ return g_gl.enableInternalTAA &&
+ !w.isPbuffer &&
+ !QD3D12_ExternalTemporalUpscalerActive(w);
}
static bool QD3D12_FinalBlitToBackBuffer(
@@ -10348,6 +10363,9 @@ void APIENTRY glEnable(GLenum cap)
case GL_FRAGMENT_PROGRAM_ARB:
QD3D12ARB_SetEnabled(cap, true);
break;
+ case GL_QD3D12_TAA:
+ QD3D12_EnableTAA(1);
+ break;
case GL_TEXTURE_2D:
#ifdef GL_TEXTURE_RECTANGLE_ARB
case GL_TEXTURE_RECTANGLE_ARB:
@@ -10385,6 +10403,9 @@ void APIENTRY glDisable(GLenum cap)
case GL_FRAGMENT_PROGRAM_ARB:
QD3D12ARB_SetEnabled(cap, false);
break;
+ case GL_QD3D12_TAA:
+ QD3D12_EnableTAA(0);
+ break;
case GL_TEXTURE_2D:
#ifdef GL_TEXTURE_RECTANGLE_ARB
case GL_TEXTURE_RECTANGLE_ARB:
@@ -10954,6 +10975,10 @@ void APIENTRY glGetIntegerv(GLenum pname, GLint* params)
*params = (GLint)QD3D12_IsDLAAEnabled();
break;
+ case GL_QD3D12_TAA_ENABLED:
+ *params = (GLint)QD3D12_IsTAAEnabled();
+ break;
+
#ifdef GL_ACTIVE_STENCIL_FACE_EXT
case GL_ACTIVE_STENCIL_FACE_EXT:
*params = (GLint)g_gl.activeStencilFace;
@@ -13561,6 +13586,12 @@ PROC WINAPI qd3d12_wglGetProcAddress(LPCSTR name) {
{ "QD3D12_IsDLAAEnabled", (PROC)QD3D12_IsDLAAEnabled },
{ "glDLAAQD3D12", (PROC)glDLAAQD3D12 },
{ "glEnableDLAAQD3D12", (PROC)glEnableDLAAQD3D12 },
+ { "QD3D12_EnableTAA", (PROC)QD3D12_EnableTAA },
+ { "QD3D12_IsTAAEnabled", (PROC)QD3D12_IsTAAEnabled },
+ { "glTAAQD3D12", (PROC)glTAAQD3D12 },
+ { "glEnableTAAQD3D12", (PROC)glEnableTAAQD3D12 },
+ { "glTemporalAAQD3D12", (PROC)glTemporalAAQD3D12 },
+ { "glEnableTemporalAAQD3D12", (PROC)glEnableTemporalAAQD3D12 },
{ "QD3D12_SetPathTracingQuality", (PROC)QD3D12_SetPathTracingQuality },
{ "QD3D12_SetPathTracingFallbackSamples", (PROC)QD3D12_SetPathTracingFallbackSamples },
{ "glSetTopLevelAccelStructureVisible", (PROC)glSetTopLevelAccelStructureVisible },
@@ -14503,6 +14534,56 @@ void APIENTRY glEnableDLAAQD3D12(GLboolean enable)
glDLAAQD3D12(enable);
}
+static void QD3D12_ResetInternalTAAHistoryForWindow(QD3D12Window* window)
+{
+ if (!window)
+ return;
+
+ for (UINT i = 0; i < QD3D12_FrameCount; ++i)
+ window->taaHistoryValid[i] = false;
+}
+
+void QD3D12_EnableTAA(int enabled)
+{
+ const bool newValue = enabled ? true : false;
+ if (g_gl.enableInternalTAA == newValue)
+ return;
+
+ g_gl.enableInternalTAA = newValue;
+ g_gl.motionHistoryReset = true;
+
+ // TAA history is only valid for the previous TAA mode. Drop it so re-enabling
+ // starts from the current frame instead of blending against stale history.
+ QD3D12_ResetInternalTAAHistoryForWindow(g_currentWindow);
+ for (auto& kv : g_windows)
+ QD3D12_ResetInternalTAAHistoryForWindow(&kv.second);
+}
+
+int QD3D12_IsTAAEnabled(void)
+{
+ return g_gl.enableInternalTAA ? 1 : 0;
+}
+
+void APIENTRY glTAAQD3D12(GLboolean enable)
+{
+ QD3D12_EnableTAA(enable != GL_FALSE ? 1 : 0);
+}
+
+void APIENTRY glEnableTAAQD3D12(GLboolean enable)
+{
+ glTAAQD3D12(enable);
+}
+
+void APIENTRY glTemporalAAQD3D12(GLboolean enable)
+{
+ glTAAQD3D12(enable);
+}
+
+void APIENTRY glEnableTemporalAAQD3D12(GLboolean enable)
+{
+ glTAAQD3D12(enable);
+}
+
void QD3D12_SetUpscalerSharpness(float sharpness)
{
g_gl.upscalerSharpness = ClampValue(sharpness, 0.0f, 1.0f);
@@ -15679,6 +15760,7 @@ GLboolean APIENTRY glIsEnabled(GLenum cap)
case GL_STENCIL_TEST_TWO_SIDE_EXT: return g_gl.stencilTwoSide ? GL_TRUE : GL_FALSE;
#endif
case GL_TEXTURE_2D: return g_gl.texture2D[g_gl.activeTextureUnit] ? GL_TRUE : GL_FALSE;
+ case GL_QD3D12_TAA: return QD3D12_IsTAAEnabled() ? GL_TRUE : GL_FALSE;
case GL_LIGHTING:
return g_glState.lightingEnabled;
diff --git a/neo/engine/opengl/opengl.h b/neo/engine/opengl/opengl.h
index 6a4f20ec..1268284f 100644
--- a/neo/engine/opengl/opengl.h
+++ b/neo/engine/opengl/opengl.h
@@ -2325,4 +2325,6 @@ void APIENTRY glShowAllTopLevelAccelStructures(
glRaytracingSceneHandle_t scene);
void QD3D12_SetUpscalerSharpness(float sharpness);
-void QD3D12_SetToneMapBrightness(float brightness);
\ No newline at end of file
+void QD3D12_SetToneMapBrightness(float brightness);
+
+void QD3D12_EnableTAA(int enabled);
\ No newline at end of file
diff --git a/neo/engine/opengl/opengl.vcxproj b/neo/engine/opengl/opengl.vcxproj
index d8c3f02f..0f4d2838 100644
--- a/neo/engine/opengl/opengl.vcxproj
+++ b/neo/engine/opengl/opengl.vcxproj
@@ -35,6 +35,7 @@
+
Disabled
@@ -178,6 +179,7 @@
+
diff --git a/neo/engine/opengl/opengl.vcxproj.filters b/neo/engine/opengl/opengl.vcxproj.filters
index 459f815a..d6e5bc58 100644
--- a/neo/engine/opengl/opengl.vcxproj.filters
+++ b/neo/engine/opengl/opengl.vcxproj.filters
@@ -42,6 +42,7 @@
tess
+
@@ -94,6 +95,7 @@
tess
+
diff --git a/neo/engine/sys/win32/win_input.cpp b/neo/engine/sys/win32/win_input.cpp
index d8aa6997..4c73741d 100644
--- a/neo/engine/sys/win32/win_input.cpp
+++ b/neo/engine/sys/win32/win_input.cpp
@@ -512,7 +512,7 @@ void IN_InitDirectInput( void ) {
// Register with the DirectInput subsystem and get a pointer
// to a IDirectInput interface we can use.
// Create the base DirectInput object
- if ( FAILED( hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&win32.g_pdi, NULL ) ) ) {
+ if ( FAILED( hr = IceDirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&win32.g_pdi, NULL ) ) ) {
common->Printf ("DirectInputCreate failed\n");
}
}
diff --git a/neo/engine/sys/win32/win_wndproc.cpp b/neo/engine/sys/win32/win_wndproc.cpp
index 7e774f2f..5f8b563e 100644
--- a/neo/engine/sys/win32/win_wndproc.cpp
+++ b/neo/engine/sys/win32/win_wndproc.cpp
@@ -276,6 +276,10 @@ main window procedure
LONG WINAPI MainWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) {
int key;
switch( uMsg ) {
+ case WM_INPUT:
+ IceDirectInputShim_HandleRawInput(lParam);
+ return 0;
+
case WM_WINDOWPOSCHANGED:
if (glConfig.isInitialized) {
RECT rect;
@@ -334,6 +338,8 @@ LONG WINAPI MainWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) {
if ( fActive == WA_INACTIVE ) {
win32.movingWindow = false;
+ Sys_GrabMouseCursor(false);
+ idKeyInput::ClearStates();
}
// start playing the game sound world
diff --git a/neo/engine/tools/radiant/CamWnd.cpp b/neo/engine/tools/radiant/CamWnd.cpp
index 87d01e05..72c03cd3 100644
--- a/neo/engine/tools/radiant/CamWnd.cpp
+++ b/neo/engine/tools/radiant/CamWnd.cpp
@@ -1829,7 +1829,7 @@ void CCamWnd::OnPaint() {
CamWnd_EnsureMenuBar(this);
CamWnd_LayoutMenuBar(this);
- idGraphicsDeviceContextHelper context(dc.m_hDC, hglrc);
+ idGraphicsDeviceContextHelper context(dc.m_hDC, hglrc, true);
g_pSplitList = NULL;
if (g_bClipMode) {
diff --git a/neo/engine/tools/radiant/MainFrm.cpp b/neo/engine/tools/radiant/MainFrm.cpp
index b0f9bf4e..fb6696e4 100644
--- a/neo/engine/tools/radiant/MainFrm.cpp
+++ b/neo/engine/tools/radiant/MainFrm.cpp
@@ -278,7 +278,6 @@ SCommandInfo g_Commands[] = {
{ "Find_Entity", VK_F3, RAD_CONTROL, ID_MISC_FINDORREPLACEENTITY},
{ "Find_NextEntity", VK_F3,RAD_SHIFT, ID_MISC_FINDNEXTENT},
- { "_ShowDOOM", VK_F2, 0, ID_SHOW_DOOM },
{ "Rotate_MouseRotate", 'R', 0, ID_SELECT_MOUSEROTATE },
{ "Rotate_ToggleFlatRotation", 'R', RAD_CONTROL, ID_VIEW_CAMERAUPDATE },
@@ -1360,12 +1359,71 @@ BOOL CMainFrame::CreateEmbeddedMainToolBar(UINT nID) {
}
m_wndToolBar.SetOwner(this);
+ RemoveDoomToolbarButton();
if (m_pXYDockWnd) {
m_pXYDockWnd->SetEmbeddedToolBar(&m_wndToolBar);
}
return TRUE;
}
+/*
+ =======================================================================================================================
+ =======================================================================================================================
+ */
+static BOOL RemoveMenuCommandRecursive(HMENU hMenu, UINT nCommandID) {
+ if (!hMenu) {
+ return FALSE;
+ }
+
+ BOOL removed = FALSE;
+ for (int i = ::GetMenuItemCount(hMenu) - 1; i >= 0; --i) {
+ MENUITEMINFO info;
+ memset(&info, 0, sizeof(info));
+ info.cbSize = sizeof(info);
+ info.fMask = MIIM_ID | MIIM_SUBMENU;
+
+ if (!::GetMenuItemInfo(hMenu, i, TRUE, &info)) {
+ continue;
+ }
+
+ if (info.hSubMenu) {
+ removed |= RemoveMenuCommandRecursive(info.hSubMenu, nCommandID);
+ if (::GetMenuItemCount(info.hSubMenu) == 0) {
+ ::DeleteMenu(hMenu, i, MF_BYPOSITION);
+ removed = TRUE;
+ }
+ }
+
+ if (!info.hSubMenu && info.wID == nCommandID) {
+ ::DeleteMenu(hMenu, i, MF_BYPOSITION);
+ removed = TRUE;
+ }
+ }
+
+ return removed;
+}
+
+void CMainFrame::RemoveDoomMenuItems(HMENU hMenu) {
+ if (hMenu) {
+ RemoveMenuCommandRecursive(hMenu, ID_SHOW_DOOM);
+ }
+}
+
+void CMainFrame::RemoveDoomToolbarButton() {
+ if (!m_wndToolBar.GetSafeHwnd()) {
+ return;
+ }
+
+ CToolBarCtrl &toolBarCtrl = m_wndToolBar.GetToolBarCtrl();
+ for (int i = toolBarCtrl.GetButtonCount() - 1; i >= 0; --i) {
+ TBBUTTON button;
+ memset(&button, 0, sizeof(button));
+ if (toolBarCtrl.GetButton(i, &button) && button.idCommand == ID_SHOW_DOOM) {
+ toolBarCtrl.DeleteButton(i);
+ }
+ }
+}
+
/*
=======================================================================================================================
=======================================================================================================================
@@ -1384,6 +1442,7 @@ void CMainFrame::MoveFrameMenuIntoXYWnd() {
}
m_hMovedMenu = hMenu;
+ RemoveDoomMenuItems(hMenu);
m_pXYDockWnd->SetMenuHandle(hMenu);
if (::GetMenu(GetSafeHwnd()) == hMenu) {
@@ -1409,6 +1468,62 @@ void CMainFrame::RecalcMainLayout() {
RecalcXYDockLayout();
}
+void CMainFrame::AttachDoomWindowToGameTab() {
+ if (!m_pXYDockWnd || !m_pXYDockWnd->GetSafeHwnd()) {
+ return;
+ }
+ if (!win32.hWnd || !::IsWindow(win32.hWnd)) {
+ return;
+ }
+
+ if (m_pXYDockWnd->GetDockedGameWindow() != win32.hWnd) {
+ m_pXYDockWnd->AttachGameWindow(win32.hWnd);
+ RecalcXYDockLayout();
+ } else {
+ m_pXYDockWnd->AttachGameWindow(win32.hWnd);
+ }
+}
+
+bool CMainFrame::IsDockedGameInputActive() const {
+ return (m_pXYDockWnd &&
+ m_pXYDockWnd->GetSafeHwnd() &&
+ m_pXYDockWnd->IsGameTabActive() &&
+ m_pXYDockWnd->IsGameWindowDocked());
+}
+
+BOOL CMainFrame::ForwardDockedGameKey(UINT message, WPARAM wParam, LPARAM lParam) {
+ if (!IsDockedGameInputActive()) {
+ return FALSE;
+ }
+
+ HWND hGameWnd = m_pXYDockWnd->GetDockedGameWindow();
+ if (!hGameWnd || !::IsWindow(hGameWnd)) {
+ return FALSE;
+ }
+
+ HWND hFocus = ::GetFocus();
+ if (hFocus != hGameWnd && !::IsChild(hGameWnd, hFocus)) {
+ ::SetFocus(hGameWnd);
+ }
+
+ ::PostMessage(hGameWnd, message, wParam, lParam);
+ return TRUE;
+}
+
+void CMainFrame::ApplyActiveTabInputMode() {
+ AttachDoomWindowToGameTab();
+
+ if (IsDockedGameInputActive()) {
+ common->ActivateTool(false);
+ m_pXYDockWnd->FocusGameWindow();
+ } else {
+ common->ActivateTool(true);
+ if (soundSystem) {
+ soundSystem->SetPlayingSoundWorld(g_qeglobals.sw);
+ }
+ }
+}
+
/*
=======================================================================================================================
=======================================================================================================================
@@ -1971,6 +2086,12 @@ void CMainFrame::CreateQEChildren() {
=======================================================================================================================
*/
BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam) {
+ if (IsDockedGameInputActive()) {
+ const UINT commandID = LOWORD(wParam);
+ if (commandID != ID_SHOW_DOOM) {
+ return FALSE;
+ }
+ }
return CFrameWnd::OnCommand(wParam, lParam);
}
@@ -2049,6 +2170,11 @@ bool MouseDown() {
void CMainFrame::OnTimer(UINT_PTR nIDEvent) {
static bool autoSavePending = false;
+ if (nIDEvent == QE_TIMER0) {
+ AttachDoomWindowToGameTab();
+ ApplyActiveTabInputMode();
+ }
+
if ( nIDEvent == QE_TIMER0 && !MouseDown() ) {
QE_CountBrushesAndUpdateStatusBar();
}
@@ -2118,6 +2244,10 @@ void CMainFrame::OnDestroy() {
SaveWindowPlacement(GetSafeHwnd(), "radiant_MainWindowPlace");
+ if (m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) {
+ m_pXYDockWnd->DetachGameWindow(TRUE);
+ }
+
if (m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) {
SaveWindowPlacement(m_pXYDockWnd->GetSafeHwnd(), "radiant_xywindow");
} else if (m_pXYWnd && m_pXYWnd->GetSafeHwnd()) {
@@ -2241,6 +2371,11 @@ void CMainFrame::OnClose() {
=======================================================================================================================
*/
void CMainFrame::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags) {
+ if (IsDockedGameInputActive()) {
+ ForwardDockedGameKey(WM_KEYUP, nChar, MAKELPARAM(nRepCnt, nFlags));
+ return;
+ }
+
// run through our list to see if we have a handler for nChar
for (int i = 0; i < g_nCommandCount; i++) {
@@ -2312,6 +2447,11 @@ bool CamOK(unsigned int nKey) {
=======================================================================================================================
*/
void CMainFrame::OnSysKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
+ if (IsDockedGameInputActive()) {
+ ForwardDockedGameKey(WM_SYSKEYDOWN, nChar, MAKELPARAM(nRepCnt, nFlags));
+ return;
+ }
+
// OnKeyDown(nChar, nRepCnt, nFlags);
if (nChar == VK_DOWN) {
OnKeyDown(nChar, nRepCnt, nFlags);
@@ -2326,6 +2466,11 @@ void CMainFrame::OnSysKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
*/
void CMainFrame::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
+ if (IsDockedGameInputActive()) {
+ ForwardDockedGameKey(WM_KEYDOWN, nChar, MAKELPARAM(nRepCnt, nFlags));
+ return;
+ }
+
for (int i = 0; i < g_nCommandCount; i++) {
if (g_Commands[i].m_nKey == nChar) { // find a match?
// check modifiers
@@ -2422,6 +2567,8 @@ BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext *pContext) {
RecalcMainLayout();
CreateQEChildren();
+ AttachDoomWindowToGameTab();
+ ApplyActiveTabInputMode();
if (m_pXYWnd) {
m_pXYWnd->SetActive(true);
@@ -5903,6 +6050,24 @@ void CMainFrame::NudgeSelection(int nDirection, float fAmount) {
=======================================================================================================================
*/
BOOL CMainFrame::PreTranslateMessage(MSG *pMsg) {
+ if (pMsg && IsDockedGameInputActive()) {
+ const UINT message = pMsg->message;
+ const BOOL keyMessage = (message >= WM_KEYFIRST && message <= WM_KEYLAST);
+
+ if (keyMessage) {
+ HWND hGameWnd = m_pXYDockWnd->GetDockedGameWindow();
+ if (hGameWnd && ::IsWindow(hGameWnd)) {
+ if (pMsg->hwnd == hGameWnd || ::IsChild(hGameWnd, pMsg->hwnd)) {
+ return FALSE;
+ }
+
+ ::SetFocus(hGameWnd);
+ ::PostMessage(hGameWnd, pMsg->message, pMsg->wParam, pMsg->lParam);
+ return TRUE;
+ }
+ }
+ }
+
if (pMsg && m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) {
BOOL menuKey = FALSE;
@@ -7004,13 +7169,14 @@ void CMainFrame::OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized) {
CFrameWnd::OnActivate(nState, pWndOther, bMinimized);
if ( nState != WA_INACTIVE ) {
- common->ActivateTool( true );
- if (::IsWindowVisible(win32.hWnd)) {
- ::ShowWindow(win32.hWnd, SW_HIDE);
- }
+ AttachDoomWindowToGameTab();
+ ApplyActiveTabInputMode();
- // start playing the editor sound world
- soundSystem->SetPlayingSoundWorld( g_qeglobals.sw );
+ if (win32.hWnd && ::IsWindow(win32.hWnd) && ::IsWindowVisible(win32.hWnd)) {
+ if (!m_pXYDockWnd || !m_pXYDockWnd->IsGameWindowDocked()) {
+ ::ShowWindow(win32.hWnd, SW_HIDE);
+ }
+ }
}
else {
//com_editorActive = false;
@@ -7266,11 +7432,16 @@ void CMainFrame::OnPatchCombine() {
void CMainFrame::OnShowDoom()
{
- int show = ::IsWindowVisible(win32.hWnd) ? SW_HIDE : SW_NORMAL;
- if (show == SW_NORMAL) {
+ // Legacy command path. The menu/toolbar entry is removed, but if an old
+ // accelerator or external command still reaches this handler, switch to the
+ // always-attached Game tab instead of showing a popup.
+ AttachDoomWindowToGameTab();
+ if (m_pXYDockWnd && m_pXYDockWnd->GetSafeHwnd()) {
g_Inspectors->SetMode(W_TEXTURE);
+ m_pXYDockWnd->SelectGameTab();
+ ApplyActiveTabInputMode();
+ RecalcXYDockLayout();
}
- ::ShowWindow(win32.hWnd, show);
}
void CMainFrame::OnViewRendermode()
diff --git a/neo/engine/tools/radiant/MainFrm.h b/neo/engine/tools/radiant/MainFrm.h
index 7c4deae1..76f27d1a 100644
--- a/neo/engine/tools/radiant/MainFrm.h
+++ b/neo/engine/tools/radiant/MainFrm.h
@@ -117,7 +117,7 @@ protected:
// CMainFrameLayoutWnd
//
// Main workspace split:
-// [ Camera/Inspector stack ] [ splitter ] [ XYDock: menu, toolbar, Z | XY ]
+// [ Camera/Inspector stack ] [ splitter ] [ XYDock: XY tab | Game tab ]
//
// XYDock already owns the Z/XY splitter. This wrapper supplies the missing
// MainFrm-level split and gives startup proportions like the reference image.
@@ -231,6 +231,10 @@ public:
CMenu* GetMenu() const;
void RecalcMainLayout();
void RecalcXYDockLayout();
+ void AttachDoomWindowToGameTab();
+ bool IsDockedGameInputActive() const;
+ BOOL ForwardDockedGameKey(UINT message, WPARAM wParam, LPARAM lParam);
+ void ApplyActiveTabInputMode();
void SetActiveXY(CXYWnd* p)
{
@@ -275,6 +279,8 @@ protected:
void CreateQEChildren();
BOOL CreateEmbeddedMainToolBar(UINT nID);
void MoveFrameMenuIntoXYWnd();
+ void RemoveDoomMenuItems(HMENU hMenu);
+ void RemoveDoomToolbarButton();
void LoadCommandMap();
void SaveCommandMap();
void ShowMenuItemKeyBindings(CMenu *pMenu);
diff --git a/neo/engine/tools/radiant/QE3.H b/neo/engine/tools/radiant/QE3.H
index d23f97a2..ca0d5a9a 100644
--- a/neo/engine/tools/radiant/QE3.H
+++ b/neo/engine/tools/radiant/QE3.H
@@ -84,21 +84,25 @@ extern std::mutex g_graphicsDeviceContextMutex;
class idGraphicsDeviceContextHelper
{
public:
- idGraphicsDeviceContextHelper(HDC inDC, HGLRC inRC)
+ idGraphicsDeviceContextHelper(HDC inDC, HGLRC inRC, bool tsaaEnabled = false)
: dc(inDC)
, rc(inRC)
{
+ this->tsaaEnabled = tsaaEnabled;
g_graphicsDeviceContextMutex.lock();
wglMakeCurrent(dc, rc);
}
~idGraphicsDeviceContextHelper()
{
+ QD3D12_EnableTAA(tsaaEnabled);
SwapBuffers(dc);
+ QD3D12_EnableTAA(true);
g_graphicsDeviceContextMutex.unlock();
}
private:
+ bool tsaaEnabled = false;
HDC dc = nullptr;
HGLRC rc = nullptr;
};
diff --git a/neo/engine/tools/radiant/XYWnd.cpp b/neo/engine/tools/radiant/XYWnd.cpp
index deed3060..78ef6e90 100644
--- a/neo/engine/tools/radiant/XYWnd.cpp
+++ b/neo/engine/tools/radiant/XYWnd.cpp
@@ -42,6 +42,16 @@ If you have questions concerning this license or the applicable additional terms
#define WM_IDLEUPDATECMDUI 0x0363
#endif
+#define ID_XYDOCK_TABS 0x7A00
+#define ID_XYDOCK_XYPAGE 0x7A03
+#define ID_XYDOCK_GAMEPAGE 0x7A04
+
+#define ID_GAME_HOST 0x7A30
+#define ID_GAME_FILL_FIT 0x7A40
+#define ID_GAME_FILL_100 0x7A41
+#define ID_GAME_FILL_75 0x7A42
+#define ID_GAME_FILL_50 0x7A43
+
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
@@ -586,6 +596,16 @@ int CXYMDIContainerWnd::GetFixedZWidth() const {
return GetZWidth();
}
+void CXYMDIContainerWnd::FocusXYWindow() {
+ if (m_pXYWnd && m_pXYWnd->GetSafeHwnd()) {
+ m_pXYWnd->SetFocus();
+ }
+}
+
+CWnd *CXYMDIContainerWnd::GetXYWindow() const {
+ return m_pXYWnd;
+}
+
bool CXYMDIContainerWnd::HitTestSplitter(const CPoint &point) const {
return !m_rcSplitter.IsRectEmpty() && m_rcSplitter.PtInRect(point);
}
@@ -756,6 +776,421 @@ BOOL CXYMDIContainerWnd::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHAND
return FALSE;
}
+//=============================================================================
+// CGameDockWnd
+//=============================================================================
+IMPLEMENT_DYNAMIC(CGameDockWnd, CWnd)
+
+BEGIN_MESSAGE_MAP(CGameDockWnd, CWnd)
+ ON_WM_CREATE()
+ ON_WM_SIZE()
+ ON_WM_ERASEBKGND()
+ ON_WM_DESTROY()
+ ON_WM_SETFOCUS()
+ ON_BN_CLICKED(ID_GAME_FILL_FIT, OnFillFit)
+ ON_BN_CLICKED(ID_GAME_FILL_100, OnFill100)
+ ON_BN_CLICKED(ID_GAME_FILL_75, OnFill75)
+ ON_BN_CLICKED(ID_GAME_FILL_50, OnFill50)
+END_MESSAGE_MAP()
+
+CGameDockWnd::CGameDockWnd() {
+ m_hGameWnd = NULL;
+ m_hOldParent = NULL;
+ m_dwOldStyle = 0;
+ m_dwOldExStyle = 0;
+ m_nFillPercent = 0;
+ m_bActive = FALSE;
+}
+
+CGameDockWnd::~CGameDockWnd() {
+}
+
+BOOL CGameDockWnd::Create(CWnd *pParent, UINT nID) {
+ CString className = AfxRegisterWndClass(
+ CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
+ ::LoadCursor(NULL, IDC_ARROW),
+ (HBRUSH)::GetStockObject(BLACK_BRUSH),
+ NULL
+ );
+
+ return CWnd::CreateEx(
+ 0,
+ className,
+ "RadiantGameDockWnd",
+ WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
+ CRect(0, 0, 0, 0),
+ pParent,
+ nID
+ );
+}
+
+void CGameDockWnd::SendGameActivate(BOOL bActive) {
+ if (!m_hGameWnd || !::IsWindow(m_hGameWnd)) {
+ return;
+ }
+
+ //
+ // Doom 3's MainWndProc expects:
+ //
+ // LOWORD(wParam) = WA_ACTIVE / WA_INACTIVE
+ // HIWORD(wParam) = minimized flag
+ //
+ // We are not minimizing it when changing tabs, so HIWORD is FALSE.
+ //
+ const WPARAM activateParam = MAKEWPARAM(bActive ? WA_ACTIVE : WA_INACTIVE, FALSE);
+
+ //
+ // lParam is the other window involved in activation.
+ // For our embedded case, use the host when activating/deactivating.
+ //
+ HWND hOther = m_wndHost.GetSafeHwnd();
+
+ ::SendMessage(m_hGameWnd, WM_ACTIVATE, activateParam, reinterpret_cast(hOther));
+
+ common->ActivateTool(!bActive);
+
+ if (bActive) {
+ //
+ // WM_ACTIVATE updates win32.activeApp / mouse grabbing.
+ // SetFocus makes keyboard input go directly to the game HWND.
+ //
+ ::SetFocus(m_hGameWnd);
+ ::SendMessage(m_hGameWnd, WM_SETFOCUS, reinterpret_cast(hOther), 0);
+ }
+ else {
+ ::SendMessage(m_hGameWnd, WM_KILLFOCUS, reinterpret_cast(hOther), 0);
+ }
+}
+
+int CGameDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) {
+ if (CWnd::OnCreate(lpCreateStruct) == -1) {
+ return -1;
+ }
+
+ CString hostClass = AfxRegisterWndClass(
+ CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
+ ::LoadCursor(NULL, IDC_ARROW),
+ (HBRUSH)::GetStockObject(BLACK_BRUSH),
+ NULL
+ );
+
+ m_wndTitle.Create("GAME", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this);
+
+ m_btnFit.Create("FIT", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_FIT);
+ m_btn100.Create("100%", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_100);
+ m_btn75.Create("75%", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_75);
+ m_btn50.Create("50%", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_GAME_FILL_50);
+
+ if (!m_wndHost.CreateEx(
+ WS_EX_CLIENTEDGE,
+ hostClass,
+ "RadiantGameRenderHost",
+ WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
+ CRect(0, 0, 0, 0),
+ this,
+ ID_GAME_HOST
+ )) {
+ return -1;
+ }
+
+ return 0;
+}
+
+void CGameDockWnd::OnDestroy() {
+ DetachGameWindow(TRUE);
+ CWnd::OnDestroy();
+}
+
+void CGameDockWnd::OnSetFocus(CWnd *pOldWnd) {
+ CWnd::OnSetFocus(pOldWnd);
+ FocusGameWindow();
+}
+
+void CGameDockWnd::SetActive(BOOL bActive) {
+ const BOOL wasActive = m_bActive;
+ m_bActive = bActive;
+
+ if (m_hGameWnd && ::IsWindow(m_hGameWnd)) {
+ ::ShowWindow(m_hGameWnd, bActive ? SW_SHOW : SW_HIDE);
+
+ if (bActive) {
+ LayoutGameWindow();
+ ::InvalidateRect(m_hGameWnd, NULL, FALSE);
+ }
+
+ //
+ // Only synthesize WM_ACTIVATE on real active-state transitions.
+ // LayoutChildren() calls ShowActiveTab(), so without this guard the
+ // game would get repeated WA_ACTIVE messages every layout pass.
+ //
+ if (wasActive != m_bActive) {
+ SendGameActivate(m_bActive);
+ }
+
+ if (bActive) {
+ FocusGameWindow();
+ }
+ }
+
+ //
+ // Keep the engine/editor global mode in sync even if the HWND does not
+ // exist yet. This way attaching the game later starts in the correct mode.
+ //
+ if (bActive) {
+ common->ActivateTool(false);
+ }
+ else {
+ common->ActivateTool(true);
+ }
+}
+
+BOOL CGameDockWnd::IsGameWindowDocked() const {
+ return (m_hGameWnd && ::IsWindow(m_hGameWnd)) ? TRUE : FALSE;
+}
+
+HWND CGameDockWnd::GetGameWindow() const {
+ return (m_hGameWnd && ::IsWindow(m_hGameWnd)) ? m_hGameWnd : NULL;
+}
+
+void CGameDockWnd::FocusGameWindow() {
+ if (m_bActive && m_hGameWnd && ::IsWindow(m_hGameWnd)) {
+ ::SetFocus(m_hGameWnd);
+ }
+}
+
+void CGameDockWnd::AttachGameWindow(HWND hGameWnd) {
+ if (!hGameWnd || !::IsWindow(hGameWnd) || !m_wndHost.GetSafeHwnd()) {
+ return;
+ }
+
+ if (m_hGameWnd == hGameWnd) {
+ LayoutGameWindow();
+ return;
+ }
+
+ DetachGameWindow(TRUE);
+
+ m_hGameWnd = hGameWnd;
+ m_hOldParent = ::GetParent(hGameWnd);
+ m_dwOldStyle = ::GetWindowLong(hGameWnd, GWL_STYLE);
+ m_dwOldExStyle = ::GetWindowLong(hGameWnd, GWL_EXSTYLE);
+
+ ::SetParent(hGameWnd, m_wndHost.GetSafeHwnd());
+
+ LONG style = m_dwOldStyle;
+ style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
+ style |= WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
+ ::SetWindowLong(hGameWnd, GWL_STYLE, style);
+
+ LONG exStyle = m_dwOldExStyle;
+ exStyle &= ~(WS_EX_APPWINDOW | WS_EX_TOOLWINDOW | WS_EX_WINDOWEDGE | WS_EX_DLGMODALFRAME);
+ exStyle |= WS_EX_CONTROLPARENT;
+ ::SetWindowLong(hGameWnd, GWL_EXSTYLE, exStyle);
+
+ ::SetWindowPos(
+ hGameWnd,
+ NULL,
+ 0,
+ 0,
+ 0,
+ 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW
+ );
+
+ LayoutGameWindow();
+
+ ::ShowWindow(hGameWnd, m_bActive ? SW_SHOW : SW_HIDE);
+
+ Sys_GrabMouseCursor(m_bActive);
+
+ IceDI_ForceOverrideSkipCenter(!m_bActive);
+
+ if (m_bActive) {
+ //
+ // If the Game tab was already active before the HWND got attached,
+ // SetActive(TRUE) would not see a false->true transition, so force the
+ // initial activation here.
+ //
+ SendGameActivate(TRUE);
+ FocusGameWindow();
+ }
+}
+
+void CGameDockWnd::DetachGameWindow(BOOL bRestore) {
+ if (!m_hGameWnd || !::IsWindow(m_hGameWnd)) {
+ m_hGameWnd = NULL;
+ m_hOldParent = NULL;
+ m_dwOldStyle = 0;
+ m_dwOldExStyle = 0;
+ return;
+ }
+
+ HWND hGameWnd = m_hGameWnd;
+ m_hGameWnd = NULL;
+
+ ::ShowWindow(hGameWnd, SW_HIDE);
+
+ if (bRestore) {
+ ::SetParent(hGameWnd, m_hOldParent);
+ ::SetWindowLong(hGameWnd, GWL_STYLE, m_dwOldStyle);
+ ::SetWindowLong(hGameWnd, GWL_EXSTYLE, m_dwOldExStyle);
+ ::SetWindowPos(
+ hGameWnd,
+ NULL,
+ 0,
+ 0,
+ 0,
+ 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED
+ );
+ }
+
+ m_hOldParent = NULL;
+ m_dwOldStyle = 0;
+ m_dwOldExStyle = 0;
+}
+
+void CGameDockWnd::SetFillPercent(int percent) {
+ m_nFillPercent = percent;
+ LayoutGameWindow();
+}
+
+CRect CGameDockWnd::ComputeGameRect(const CRect &hostClient) const {
+ CRect rc = hostClient;
+
+ if (hostClient.Width() <= 0 || hostClient.Height() <= 0) {
+ return rc;
+ }
+
+ int targetW = hostClient.Width();
+ int targetH = hostClient.Height();
+
+ if (m_nFillPercent == 0) {
+ // Fit to a 16:9 frame while preserving aspect ratio.
+ targetW = hostClient.Width();
+ targetH = (targetW * 9) / 16;
+
+ if (targetH > hostClient.Height()) {
+ targetH = hostClient.Height();
+ targetW = (targetH * 16) / 9;
+ }
+ } else {
+ targetW = (hostClient.Width() * m_nFillPercent) / 100;
+ targetH = (hostClient.Height() * m_nFillPercent) / 100;
+ }
+
+ if (targetW < 1) {
+ targetW = 1;
+ }
+ if (targetH < 1) {
+ targetH = 1;
+ }
+
+ int x = hostClient.left + (hostClient.Width() - targetW) / 2;
+ int y = hostClient.top + (hostClient.Height() - targetH) / 2;
+
+ rc.SetRect(x, y, x + targetW, y + targetH);
+ return rc;
+}
+
+void CGameDockWnd::LayoutGameWindow() {
+ if (!m_hGameWnd || !::IsWindow(m_hGameWnd) || !m_wndHost.GetSafeHwnd()) {
+ return;
+ }
+
+ CRect hostClient;
+ m_wndHost.GetClientRect(hostClient);
+
+ CRect gameRect = ComputeGameRect(hostClient);
+
+ ::MoveWindow(
+ m_hGameWnd,
+ gameRect.left,
+ gameRect.top,
+ gameRect.Width(),
+ gameRect.Height(),
+ TRUE
+ );
+
+ ::InvalidateRect(m_hGameWnd, NULL, FALSE);
+}
+
+void CGameDockWnd::LayoutChildren() {
+ if (!GetSafeHwnd()) {
+ return;
+ }
+
+ CRect client;
+ GetClientRect(client);
+
+ const int stripH = 34;
+ const int gap = 6;
+ int x = client.left + 8;
+ int y = client.top + 4;
+
+ if (m_wndTitle.GetSafeHwnd()) {
+ m_wndTitle.MoveWindow(x, y, 90, 24, TRUE);
+ x += 100;
+ }
+
+ CWnd *buttons[] = { &m_btnFit, &m_btn100, &m_btn75, &m_btn50 };
+ for (int i = 0; i < 4; i++) {
+ if (buttons[i]->GetSafeHwnd()) {
+ buttons[i]->MoveWindow(x, y, 54, 24, TRUE);
+ x += 54 + gap;
+ }
+ }
+
+ CRect hostRect(
+ client.left + 4,
+ client.top + stripH + 4,
+ client.right - 4,
+ client.bottom - 4
+ );
+
+ if (hostRect.Width() < 1 || hostRect.Height() < 1) {
+ return;
+ }
+
+ if (m_wndHost.GetSafeHwnd()) {
+ m_wndHost.MoveWindow(hostRect, TRUE);
+ }
+
+ LayoutGameWindow();
+}
+
+void CGameDockWnd::OnSize(UINT nType, int cx, int cy) {
+ CWnd::OnSize(nType, cx, cy);
+ LayoutChildren();
+}
+
+BOOL CGameDockWnd::OnEraseBkgnd(CDC *pDC) {
+ CRect client;
+ GetClientRect(client);
+ pDC->FillSolidRect(client, RGB(18, 18, 18));
+ return TRUE;
+}
+
+void CGameDockWnd::OnFillFit() {
+ SetFillPercent(0);
+ FocusGameWindow();
+}
+
+void CGameDockWnd::OnFill100() {
+ SetFillPercent(100);
+ FocusGameWindow();
+}
+
+void CGameDockWnd::OnFill75() {
+ SetFillPercent(75);
+ FocusGameWindow();
+}
+
+void CGameDockWnd::OnFill50() {
+ SetFillPercent(50);
+ FocusGameWindow();
+}
+
//=============================================================================
// CXYDockWnd
//=============================================================================
@@ -766,11 +1201,13 @@ BEGIN_MESSAGE_MAP(CXYDockWnd, CWnd)
ON_WM_SIZE()
ON_WM_ERASEBKGND()
ON_MESSAGE(WM_IDLEUPDATECMDUI, OnIdleUpdateCmdUI)
+ ON_NOTIFY(TCN_SELCHANGE, ID_XYDOCK_TABS, OnTabSelChange)
END_MESSAGE_MAP()
CXYDockWnd::CXYDockWnd() {
m_pToolBar = NULL;
m_bInLayout = false;
+ m_nActiveTab = XYDOCK_TAB_XY;
}
CXYDockWnd::~CXYDockWnd() {
@@ -803,15 +1240,69 @@ int CXYDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if (CWnd::OnCreate(lpCreateStruct) == -1) {
return -1;
}
- if (!m_wndMenuBar.Create(this, 0x7A01)) {
+
+ if (!m_wndTabs.Create(
+ WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | TCS_TABS | TCS_SINGLELINE,
+ CRect(0, 0, 0, 0),
+ this,
+ ID_XYDOCK_TABS
+ )) {
return -1;
}
- if (!m_wndMDIContainer.Create(this, 0x7A02)) {
+
+ TCITEM item;
+ memset(&item, 0, sizeof(item));
+ item.mask = TCIF_TEXT;
+
+ item.pszText = "XY";
+ m_wndTabs.InsertItem(XYDOCK_TAB_XY, &item);
+
+ item.pszText = "Game";
+ m_wndTabs.InsertItem(XYDOCK_TAB_GAME, &item);
+
+ CString pageClass = AfxRegisterWndClass(
+ CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
+ ::LoadCursor(NULL, IDC_ARROW),
+ (HBRUSH)(COLOR_BTNFACE + 1),
+ NULL
+ );
+
+ if (!m_wndXYPage.CreateEx(
+ 0,
+ pageClass,
+ "RadiantXYTabPage",
+ WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
+ CRect(0, 0, 0, 0),
+ &m_wndTabs,
+ ID_XYDOCK_XYPAGE
+ )) {
return -1;
}
+
+ if (!m_wndGamePage.Create(&m_wndTabs, ID_XYDOCK_GAMEPAGE)) {
+ return -1;
+ }
+ m_wndGamePage.ShowWindow(SW_HIDE);
+
+ if (!m_wndMenuBar.Create(&m_wndXYPage, 0x7A01)) {
+ return -1;
+ }
+
+ if (!m_wndMDIContainer.Create(&m_wndXYPage, 0x7A02)) {
+ return -1;
+ }
+
+ m_wndTabs.SetCurSel(XYDOCK_TAB_XY);
return 0;
}
+CWnd *CXYDockWnd::GetXYPageParent() const {
+ if (m_wndXYPage.GetSafeHwnd()) {
+ return const_cast(&m_wndXYPage);
+ }
+ return const_cast(this);
+}
+
void CXYDockWnd::SetChildWindows(CXYWnd *pXYWnd, CZWnd *pZWnd) {
m_wndMDIContainer.SetChildWindows(pXYWnd, pZWnd);
LayoutChildren();
@@ -825,15 +1316,14 @@ void CXYDockWnd::SetEmbeddedWindows(CWnd *pZWnd, CWnd *pXYWnd) {
void CXYDockWnd::SetToolBar(CToolBar *pToolBar) {
m_pToolBar = pToolBar;
if (m_pToolBar && m_pToolBar->GetSafeHwnd()) {
- CWnd *commandTarget = GetCommandTarget();
- if (!commandTarget || !commandTarget->GetSafeHwnd()) {
- commandTarget = this;
- }
-
- m_pToolBar->SetParent(this);
+ CWnd *toolbarParent = GetXYPageParent();
+ m_pToolBar->SetParent(toolbarParent);
m_pToolBar->SetOwner(this);
m_pToolBar->SetDlgCtrlID(AFX_IDW_TOOLBAR);
- m_pToolBar->ModifyStyle(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
+ m_pToolBar->ModifyStyle(
+ WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU,
+ WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN
+ );
}
LayoutChildren();
}
@@ -858,6 +1348,10 @@ CWnd *CXYDockWnd::GetCommandTarget() const {
}
void CXYDockWnd::UpdateToolBarCmdUI(BOOL bDisableIfNoHndler) {
+ if (m_nActiveTab != XYDOCK_TAB_XY) {
+ return;
+ }
+
if (!m_pToolBar || !m_pToolBar->GetSafeHwnd() || !m_pToolBar->IsWindowVisible()) {
return;
}
@@ -923,6 +1417,121 @@ int CXYDockWnd::GetZPercent() const {
return GetZDockPercent();
}
+void CXYDockWnd::ShowActiveTab() {
+ const BOOL xyActive = (m_nActiveTab == XYDOCK_TAB_XY);
+ const BOOL gameActive = (m_nActiveTab == XYDOCK_TAB_GAME);
+
+ if (m_wndXYPage.GetSafeHwnd()) {
+ m_wndXYPage.ShowWindow(xyActive ? SW_SHOW : SW_HIDE);
+ if (xyActive) {
+ m_wndXYPage.SetWindowPos(&wndTop, 0, 0, 0, 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ }
+ }
+
+ if (m_wndGamePage.GetSafeHwnd()) {
+ m_wndGamePage.ShowWindow(gameActive ? SW_SHOW : SW_HIDE);
+ if (gameActive) {
+ m_wndGamePage.SetWindowPos(&wndTop, 0, 0, 0, 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ }
+ m_wndGamePage.SetActive(gameActive);
+ }
+}
+
+void CXYDockWnd::SetActiveTab(int nTab) {
+ if (nTab != XYDOCK_TAB_XY && nTab != XYDOCK_TAB_GAME) {
+ nTab = XYDOCK_TAB_XY;
+ }
+
+ const BOOL changed = (m_nActiveTab != nTab);
+ m_nActiveTab = nTab;
+
+ if (m_wndTabs.GetSafeHwnd()) {
+ m_wndTabs.SetCurSel(nTab);
+ }
+
+ LayoutChildren();
+
+ if (changed) {
+ if (m_nActiveTab == XYDOCK_TAB_GAME) {
+ //
+ // Game tab selected:
+ // - editor tool input off
+ // - game HWND receives WM_ACTIVATE/WA_ACTIVE through SetActive(TRUE)
+ // - keyboard/mouse focus moves to game
+ //
+ common->ActivateTool(false);
+
+ if (m_wndGamePage.GetSafeHwnd()) {
+ m_wndGamePage.SetActive(TRUE);
+ }
+
+ FocusGameWindow();
+ }
+ else {
+ //
+ // XY tab selected:
+ // - game HWND receives WM_ACTIVATE/WA_INACTIVE through SetActive(FALSE)
+ // - editor tool input comes back
+ // - focus returns to XY
+ //
+ if (m_wndGamePage.GetSafeHwnd()) {
+ m_wndGamePage.SetActive(FALSE);
+ }
+
+ common->ActivateTool(true);
+ FocusXYWindow();
+ }
+ }
+}
+
+void CXYDockWnd::OnTabSelChange(NMHDR *pNMHDR, LRESULT *pResult) {
+ int sel = m_wndTabs.GetCurSel();
+ SetActiveTab(sel);
+
+ if (pResult) {
+ *pResult = 0;
+ }
+}
+
+void CXYDockWnd::SelectXYTab() {
+ SetActiveTab(XYDOCK_TAB_XY);
+}
+
+void CXYDockWnd::SelectGameTab() {
+ SetActiveTab(XYDOCK_TAB_GAME);
+}
+
+BOOL CXYDockWnd::IsGameTabActive() const {
+ return (m_nActiveTab == XYDOCK_TAB_GAME) ? TRUE : FALSE;
+}
+
+void CXYDockWnd::AttachGameWindow(HWND hGameWnd) {
+ m_wndGamePage.AttachGameWindow(hGameWnd);
+ m_wndGamePage.SetActive(IsGameTabActive());
+}
+
+void CXYDockWnd::DetachGameWindow(BOOL bRestore) {
+ m_wndGamePage.DetachGameWindow(bRestore);
+}
+
+BOOL CXYDockWnd::IsGameWindowDocked() const {
+ return m_wndGamePage.IsGameWindowDocked();
+}
+
+HWND CXYDockWnd::GetDockedGameWindow() const {
+ return m_wndGamePage.GetGameWindow();
+}
+
+void CXYDockWnd::FocusGameWindow() {
+ m_wndGamePage.FocusGameWindow();
+}
+
+void CXYDockWnd::FocusXYWindow() {
+ m_wndMDIContainer.FocusXYWindow();
+}
+
void CXYDockWnd::LayoutChildren() {
if (!GetSafeHwnd() || m_bInLayout) {
return;
@@ -933,41 +1542,77 @@ void CXYDockWnd::LayoutChildren() {
CRect client;
GetClientRect(client);
- int y = client.top;
- const int width = client.Width();
-
- if (m_wndMenuBar.GetSafeHwnd()) {
- if (m_wndMenuBar.GetMenuHandle()) {
- m_wndMenuBar.ShowWindow(SW_SHOW);
- const int menuHeight = m_wndMenuBar.PreferredHeight();
- m_wndMenuBar.MoveWindow(client.left, y, width, menuHeight, TRUE);
- y += menuHeight;
- } else {
- m_wndMenuBar.ShowWindow(SW_HIDE);
- }
- }
-
- if (m_pToolBar && m_pToolBar->GetSafeHwnd()) {
- if (m_pToolBar->IsWindowVisible()) {
- CSize toolbarSize = m_pToolBar->CalcFixedLayout(FALSE, TRUE);
- int toolbarHeight = toolbarSize.cy;
- if (toolbarHeight < 24) {
- toolbarHeight = 24;
- }
- m_pToolBar->MoveWindow(client.left, y, width, toolbarHeight, TRUE);
- y += toolbarHeight;
- }
- }
-
- CRect content(client.left, y, client.right, client.bottom);
- if (content.Width() < 1 || content.Height() < 1) {
+ if (client.Width() < 1 || client.Height() < 1) {
m_bInLayout = false;
return;
}
- if (m_wndMDIContainer.GetSafeHwnd()) {
- m_wndMDIContainer.MoveWindow(content, TRUE);
- m_wndMDIContainer.LayoutChildren();
+ CRect pageRect(0, 0, client.Width(), client.Height());
+
+ if (m_wndTabs.GetSafeHwnd()) {
+ m_wndTabs.MoveWindow(client, TRUE);
+
+ // The tab pages are children of the tab control, so use tab-client
+ // coordinates here. Creating/moving them as siblings of the tab control
+ // can leave them behind the tab control's client area, which makes both
+ // tabs look empty even though their child windows exist.
+ m_wndTabs.GetClientRect(&pageRect);
+ m_wndTabs.AdjustRect(FALSE, &pageRect);
+ pageRect.DeflateRect(2, 2);
+ }
+
+ if (m_wndXYPage.GetSafeHwnd()) {
+ m_wndXYPage.MoveWindow(pageRect, TRUE);
+ }
+
+ if (m_wndGamePage.GetSafeHwnd()) {
+ m_wndGamePage.MoveWindow(pageRect, TRUE);
+ m_wndGamePage.LayoutChildren();
+ }
+
+ ShowActiveTab();
+
+ if (m_pToolBar && m_pToolBar->GetSafeHwnd() && m_wndXYPage.GetSafeHwnd()) {
+ if (::GetParent(m_pToolBar->GetSafeHwnd()) != m_wndXYPage.GetSafeHwnd()) {
+ m_pToolBar->SetParent(&m_wndXYPage);
+ }
+ }
+
+ if (m_wndXYPage.GetSafeHwnd()) {
+ CRect xyClient;
+ m_wndXYPage.GetClientRect(xyClient);
+
+ int y = xyClient.top;
+ const int width = xyClient.Width();
+
+ if (m_wndMenuBar.GetSafeHwnd()) {
+ if (m_wndMenuBar.GetMenuHandle()) {
+ m_wndMenuBar.ShowWindow(SW_SHOW);
+ const int menuHeight = m_wndMenuBar.PreferredHeight();
+ m_wndMenuBar.MoveWindow(xyClient.left, y, width, menuHeight, TRUE);
+ y += menuHeight;
+ } else {
+ m_wndMenuBar.ShowWindow(SW_HIDE);
+ }
+ }
+
+ if (m_pToolBar && m_pToolBar->GetSafeHwnd()) {
+ if (m_pToolBar->IsWindowVisible()) {
+ CSize toolbarSize = m_pToolBar->CalcFixedLayout(FALSE, TRUE);
+ int toolbarHeight = toolbarSize.cy;
+ if (toolbarHeight < 24) {
+ toolbarHeight = 24;
+ }
+ m_pToolBar->MoveWindow(xyClient.left, y, width, toolbarHeight, TRUE);
+ y += toolbarHeight;
+ }
+ }
+
+ CRect content(xyClient.left, y, xyClient.right, xyClient.bottom);
+ if (content.Width() >= 1 && content.Height() >= 1 && m_wndMDIContainer.GetSafeHwnd()) {
+ m_wndMDIContainer.MoveWindow(content, TRUE);
+ m_wndMDIContainer.LayoutChildren();
+ }
}
m_bInLayout = false;
@@ -978,6 +1623,10 @@ void CXYDockWnd::RecalcLayout() {
}
BOOL CXYDockWnd::TrackMenuMnemonic(UINT nChar) {
+ if (m_nActiveTab != XYDOCK_TAB_XY) {
+ return FALSE;
+ }
+
if (m_wndMenuBar.GetSafeHwnd()) {
return m_wndMenuBar.TrackMnemonic(nChar);
}
@@ -999,6 +1648,10 @@ LRESULT CXYDockWnd::OnIdleUpdateCmdUI(WPARAM wParam, LPARAM lParam) {
}
BOOL CXYDockWnd::OnCommand(WPARAM wParam, LPARAM lParam) {
+ if (m_nActiveTab != XYDOCK_TAB_XY) {
+ return CWnd::OnCommand(wParam, lParam);
+ }
+
CWnd *commandTarget = GetCommandTarget();
if (commandTarget && commandTarget->GetSafeHwnd() && commandTarget != this) {
return static_cast(commandTarget->SendMessage(WM_COMMAND, wParam, lParam));
@@ -1007,6 +1660,10 @@ BOOL CXYDockWnd::OnCommand(WPARAM wParam, LPARAM lParam) {
}
BOOL CXYDockWnd::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT *pResult) {
+ if (wParam == ID_XYDOCK_TABS) {
+ return CWnd::OnNotify(wParam, lParam, pResult);
+ }
+
CWnd *commandTarget = GetCommandTarget();
if (commandTarget && commandTarget->GetSafeHwnd() && commandTarget != this) {
LRESULT result = commandTarget->SendMessage(WM_NOTIFY, wParam, lParam);
@@ -1026,6 +1683,10 @@ BOOL CXYDockWnd::OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO
return TRUE;
}
+ if (m_nActiveTab != XYDOCK_TAB_XY) {
+ return FALSE;
+ }
+
CWnd *commandTarget = GetCommandTarget();
if (commandTarget && commandTarget->GetSafeHwnd() && commandTarget != this) {
return commandTarget->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
diff --git a/neo/engine/tools/radiant/XYWnd.h b/neo/engine/tools/radiant/XYWnd.h
index 11af5920..db08b643 100644
--- a/neo/engine/tools/radiant/XYWnd.h
+++ b/neo/engine/tools/radiant/XYWnd.h
@@ -39,6 +39,7 @@ If you have questions concerning this license or the applicable additional terms
#include "qe3.h"
#include "CamWnd.h"
+#include
class CXYWnd;
@@ -109,6 +110,8 @@ public:
int GetZDockPercent() const;
int GetZWidth() const;
int GetFixedZWidth() const;
+ void FocusXYWindow();
+ CWnd *GetXYWindow() const;
protected:
CWnd *m_pXYWnd;
@@ -143,16 +146,83 @@ protected:
DECLARE_MESSAGE_MAP()
};
+//=============================================================================
+// CGameDockWnd
+//
+// Docked game preview page used by CXYDockWnd's Game tab. This class owns a
+// small command strip and a black child host window. The real game HWND is
+// reparented into the host so the existing renderer/context can keep using the
+// same window instead of a popup.
+//=============================================================================
+class CGameDockWnd : public CWnd
+{
+ DECLARE_DYNAMIC(CGameDockWnd)
+
+public:
+ CGameDockWnd();
+ virtual ~CGameDockWnd();
+
+ BOOL Create(CWnd *pParent, UINT nID);
+ void AttachGameWindow(HWND hGameWnd);
+ void DetachGameWindow(BOOL bRestore = TRUE);
+ BOOL IsGameWindowDocked() const;
+ HWND GetGameWindow() const;
+ void FocusGameWindow();
+
+ void SetActive(BOOL bActive);
+ void LayoutChildren();
+ void SendGameActivate(BOOL bActive);
+protected:
+ CStatic m_wndTitle;
+ CButton m_btnFit;
+ CButton m_btn100;
+ CButton m_btn75;
+ CButton m_btn50;
+ CWnd m_wndHost;
+
+ HWND m_hGameWnd;
+ HWND m_hOldParent;
+ LONG m_dwOldStyle;
+ LONG m_dwOldExStyle;
+
+ int m_nFillPercent; // 0 = fit 16:9, otherwise percentage of host area
+ BOOL m_bActive;
+
+ void SetFillPercent(int percent);
+ void LayoutGameWindow();
+ CRect ComputeGameRect(const CRect &hostClient) const;
+
+ afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
+ afx_msg void OnSize(UINT nType, int cx, int cy);
+ afx_msg BOOL OnEraseBkgnd(CDC *pDC);
+ afx_msg void OnDestroy();
+ afx_msg void OnSetFocus(CWnd *pOldWnd);
+
+ afx_msg void OnFillFit();
+ afx_msg void OnFill100();
+ afx_msg void OnFill75();
+ afx_msg void OnFill50();
+
+ DECLARE_MESSAGE_MAP()
+};
+
//=============================================================================
// CXYDockWnd
//
-// Owns the embedded XY editor layout:
+// Owns the embedded XY editor layout and the new docked game preview:
+// [ tabs: XY | Game ]
+//
+// XY tab:
// [ menu bar ]
// [ toolbar ]
// [ MDI client: Z window | splitter | XY top render window ]
//
+// Game tab:
+// [ game command strip / fill controls ]
+// [ hosted game HWND ]
+//
// The docked Z window starts at 5% of the MDI container width. The divider in
-// the MDI client can be dragged to resize the Z and XY top panes.
+// the MDI client can be dragged to resize the Z and XY top panes interactively.
//=============================================================================
class CXYDockWnd : public CWnd
{
@@ -187,16 +257,41 @@ public:
int GetZDockPercent() const;
int GetZPercent() const;
+ void AttachGameWindow(HWND hGameWnd);
+ void DetachGameWindow(BOOL bRestore = TRUE);
+ BOOL IsGameWindowDocked() const;
+ HWND GetDockedGameWindow() const;
+ BOOL IsGameTabActive() const;
+ void SelectXYTab();
+ void SelectGameTab();
+ void FocusGameWindow();
+ void FocusXYWindow();
+
protected:
+ enum {
+ XYDOCK_TAB_XY = 0,
+ XYDOCK_TAB_GAME = 1
+ };
+
+ CTabCtrl m_wndTabs;
+ CWnd m_wndXYPage;
+ CGameDockWnd m_wndGamePage;
+
CXYMenuBar m_wndMenuBar;
CXYMDIContainerWnd m_wndMDIContainer;
CToolBar *m_pToolBar;
bool m_bInLayout;
+ int m_nActiveTab;
+
+ void SetActiveTab(int nTab);
+ void ShowActiveTab();
+ CWnd *GetXYPageParent() const;
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg BOOL OnEraseBkgnd(CDC *pDC);
afx_msg LRESULT OnIdleUpdateCmdUI(WPARAM wParam, LPARAM lParam);
+ afx_msg void OnTabSelChange(NMHDR *pNMHDR, LRESULT *pResult);
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT *pResult);
virtual BOOL OnCmdMsg(UINT nID, int nCode, void *pExtra, AFX_CMDHANDLERINFO *pHandlerInfo);
diff --git a/q4base/q4config.qe4 b/q4base/q4config.qe4
new file mode 100644
index 00000000..c17fccb5
--- /dev/null
+++ b/q4base/q4config.qe4
@@ -0,0 +1,18 @@
+{
+"mapspath" "w:\doom\base\maps"
+"bsp" ""
+"bsp noflood" ""
+"bsp shadowOpt 2" ""
+"bsp noaas" ""
+"bspext" ""
+"bspext noflood" ""
+"autosave" "c:\autosave.map"
+"autosave1" "c:\autosave1.map"
+"autosave2" "c:\autosave2.map"
+"texturepath" "w:\doom\base\textures"
+"entitypath" "w:\doom\base\scripts\*.def"
+"remotebasepath" "w:\doom\base"
+"rshcmd" ""
+"basepath" "w:\doom\base"
+"brush_primit" "1"
+}