MQ2Bridge::Pollslot-name fallback paths now zero-init the localslotName[CHARSEL_NAME_LEN]buffer beforewsprintfA. The subsequentmemcpy(shm->names[i], slotName, CHARSEL_NAME_LEN)copies the full 64-byte buffer into the C#-readable shared-memory region, so any bytes past the NUL terminator written bywsprintfAwere uninitialized stack contents leaking into SHM. Not exploitable — destination size is fixed and there's no path-length blowup — but unnecessary disclosure of return addresses / local pointers. Two call sites (mq2_bridge.cpp:3342and:3372) now match the surrounding={}zero-init pattern.
- Semgrep workflow excludes
Native/from scans; thegitlab.flawfinder.*community rules generated zero-signal noise on x86 RE / MQ2-port code (37 historical findings, all manually triaged as false positives).
- Self-updater now fails closed when a release has no SHA256SUMS asset. v3.14.3 fixed the catch-block fail-open inside the integrity-check try, but the outer
if (!string.IsNullOrEmpty(_hashFileUrl))still skipped verification entirely if the release didn't ship a manifest. As of this version, an empty_hashFileUrlaborts the update with a clear error. Combined with the workflow change in v3.14.3 (which always emitsSHA256SUMS), self-update is now unconditionally fail-closed: every accepted payload has been hash-verified end-to-end.
- Self-updater no longer fails open on hash verification errors. The integrity-check
tryblock atUpdateDialog.cs:332-372previously had a barecatch { /* proceed without verification */ }that swallowed any exception from the SHA256SUMS fetch — meaning an MITM that dropped the SHA256SUMS request could silently bypass the hash check entirely. The catch now logs, deletes the partial download, shows an error, and aborts the update. Network blips that take down the hash file fetch will now require a retry rather than installing an unverified binary. - Release workflow now generates and uploads
SHA256SUMSalongside the zip bundle. Previously the workflow only attached the zip, so the entire hash-verify code path was dead on shipped releases (it required_hashFileUrlto be non-empty, which depended on aSHA256SUMSasset that was never produced). This is the first release where in-app integrity checking is actually exercised end-to-end.
- The first release on which the upgrading client will benefit from these checks is the next one after v3.14.3 — clients currently on v3.14.2 or earlier are running old updater code when they pull v3.14.3, so this release bootstraps the chain forward.
- Update moved into the persistent bottom toolbar of Settings (next to GitHub) where it's reachable from any tab — was previously buried on the Paths tab.
- Nuclear Reset moved out of the bottom toolbar onto the Paths tab (where Update used to live, between Help and Uninstall) — it's a destructive, rarely-used action and now lives alongside the other destructive Paths-tab tool (Uninstall) instead of being one fat-finger away on the always-visible toolbar. Confirm dialog and
_reopenAfterClosereopen-with-defaults flow unchanged.
AutoLoginManager.LoginAccount(LoginAccount, bool?)deleted — the[Obsolete]v3-wrapper that synthesized a v4Account(+ optionalCharacter) from a legacyLoginAccountrow and delegated toBeginLogin. All live tray paths route through the intent-explicitLoginToCharselect(Account)/LoginAndEnterWorld(Character)API as of v3.13.0–v3.14.0 — the wrapper was the last piece of v3 routing scaffolding.TrayManager.ExecuteQuickLogindeleted — the only remaining caller of the obsolete wrapper, itself documented as "now dead code — Phase 5 deletes it per plan" at the time of v3.14.0. No live tray entry routed here.
- Four stale doc comments updated:
TrayManager.BuildTeamsSubmenuno longer claims the team path routes throughExecuteQuickLogin;FireLegacyQuickLoginSlotno longer references the deleted wrapper at a staleAutoLoginManager.cs:127line;FireTeam's doc-block no longer carries the "this method bypasses the dead path" footnote;AppConfig.FindAccountByNameno longer points at a staleTrayManager.cs:1321-1322line range.
CS0618warning silenced — Release build is now warning-free.
- The
LoginAccounttype (v3 model class inModels/LoginAccount.cs) is unchanged and still consumed byLegacyAccountsconfig storage, the v3→v4 migrator, the splitter, and the Settings reverse-mapper. Only the obsolete method of the same name onAutoLoginManagerwas removed.
- Color-coded names in Configure dialogs for Teams / Characters / Accounts — match the A (purple) / C (blue) pill scheme established by team-configure. Team rows split into per-slot sub-labels by kind; Character / Account dialogs uniformly tinted. Orphan-dim and stale-warn states retained.
- Tray-menu noise reduction — removed
ToolTipTexton five root tray-menu entries (Launch Client,Launch Team,Accounts,Characters,Teamsparents). Per-item submenu tooltips kept. - Window Title card tightened — dropped the "Applied after client is in world" hint; card height shrank from 56 to 40px.
LegacyHotkeyLookupnow reads v4 hotkey lists (AccountHotkeys+CharacterHotkeys). Was Phase-3-only and silently dropped any hotkey set via the v4 dialogs (e.g.Natedogg + Alt+Iset in the Characters submenu would not display).
QuickLoginNempty → fall back to combinedCharacterHotkeysthenAccountHotkeys(populated only), positionally indexed by slot.QuickLoginNset butLegacyAccountlookup fails → try v4 Character then Account by Name (case-insensitive on this drift path only; v4-list path stays ordinal). Rescues post-migration case drift.LogFirstFirefamily strings distinguish the four routing paths so the active path is visible in logs.
- Clean Release build + FileVersion
3.14.0.02026-04-29.
- Accounts submenu now shows Username (the unique login) instead of
Account.Name. Legacy migrations had Name = character name, which collided with the Characters submenu — this removes the ambiguity. - Teams submenu shows the resolved character/account names per team (e.g.
🚀 natedogg / acpots) instead of the staticAuto-Login Team Nlabel. - Menu hover tooltips can be toggled off via Settings → Video → Preferences → Show Tooltips. Same toggle that gated balloon toasts.
FloatingTooltipnow word-wraps long messages (max width 480px) so multi-account warnings don't run off the screen.
- Position memory for 8 dialogs across the workspace: Account/Character/Team Hotkeys (Configure), Account/Character Edit (Add + Edit share), AutoLoginTeams, EQClientSettings, ProcessManager. Each remembers its last-open location for the rest of the session.
- Bug fix in
DarkTheme.StyleForm: the helper was clobbering callers'FormStartPosition.ManualwithCenterScreen. Now preserves bothCenterParentandManual.
- Bug fix in
- Paths tab Startup card restored to original padding (x=47), single row layout for
Create Desktop Shortcut+Run at Startup.Show Tooltipsmoved to Video → Preferences (paired with the Tooltip Duration knob it gates). - Hotkeys tab gained a header-less, full-width
Client Launch Delayaside at the bottom (moved from Video → Preferences). - Video tab Preferences renamed
Tooltip Delay:→Tooltip Duration:(the value is the auto-dismiss interval, not a hover delay). Range tightened to 100–5000ms with config Validate clamp matched. - Update dialog compacted from 420×210 → 320×152, symmetric 20px top/bottom pads, buttons centered under the status text. Single-button OK states (winget / error / up-to-date) also centered.
- Account / Character Hotkeys Configure dialogs gained intent hints:
Will load to Character SelectandWill load into gamerespectively. - Team Hotkeys Configure dialog row labels now show the team's resolved contents (
Team 1 — natedogg / acpots) withAutoEllipsisfor long names. Shrunk back to 400px wide after the destination suffix was dropped (see AutoLoginTeams refactor below). - Characters table
HKcolumn →Hotkey, content is now a centered green ✓ when bound (with the full combo on the cell tooltip) instead of a truncated combo string. Trim Nowbutton no longer fires a pre-work "Trimming log files…" popup; only the result MessageBox.
- Per-team
Enter Worldtoggle removed. Destination is now dictated by slot kind alone:Characterslot →LoginAndEnterWorld(character, null)→ enters the game world.Accountslot →LoginToCharselect(account)→ stops at character select.- Mixed teams (Account + Character) get a mix; each slot follows its kind.
- To stop a character at charselect, put the backing Account in that slot instead.
Team{N}AutoEnterfields removed fromAppConfig(System.Text.Json silently ignores the unknown JSON keys on load; next save drops them from the file).ResolveTeamConfigandFireTeamsimplified to drop theteamEntersWorldparameter;BuildTeamTooltipno longer adds a[force enter world]line.- Pill colors changed from value-judgment to neutral. Was
✓-green(Character) /!-yellow(Account) which read as "correct/warning"; nowC-on-blue(Character) /A-on-purple(Account) — both kinds are valid, no implied right/wrong. Unresolved still red✗since it IS an error state. Legend updated toC = Character A = Account ✗ = unresolved. - Pill tooltips reworded descriptive instead of prescriptive — Account pill no longer says "Pick a Character to enter world instead", just states the constraint.
- Form shrunk 560 → 480 wide after the Enter World column came out.
- Buttons no longer clipped at the bottom (form was 210 tall; buttons at y=184 + 30px height = ended at 214). Form is now 254 with symmetric 18px top/bottom pads and the warning label gets its own row above the buttons.
- Two-round agent verifier sweeps over the major refactor and the language pass — caught two pill-tooltip drift issues (Character / Account) that survived the first pass, plus the
UpdateDialog.ShowErrorbutton position that the dialog-shrinkreplace_allmissed. - Memory note saved on
CharacterSelector.Decideprecedence:Slot ≥ 1overrides Name lookup entirely (Case 3);Slot = 0is the only state that uses name-based heap lookup. Empirical inspection confirmed Nate's config has been running on slot-based selection (all three Characters had non-zero slots), so name-based has never been tested in his prod environment. Decision: leave Slot field as-is for now; revisit as a focused change with a dual-box smoke test.
- No user-visible behavior change vs v3.12.0. Both clients enter password and reach in-world cleanly, ~63s end-to-end (verified dual-box 2026-04-26).
Native/eqmain_widgets_mq2style.{h,cpp}— MQ2-style structural recursion throughCXWnd's TListNode + TList multiple-inheritance layout.FindLiveScreenByName+RecurseAndFindName+FindChildByNamewith heuristicCStrRepCXStr name match. Wired throughFindLivePasswordCEditWnd(ineqmain_widgets.cpp) and theLOGIN_ConnectButtonlookup (inlogin_state_machine.cpp) with legacy heap-cross-ref fallback.kMQ2StyleWidgetLookup = falsemaster toggle in the new header. Both call sites skip MQ2-style entirely; behavior matches v3.12.0 baseline.
CXWnd::pNext+0x08(TListNode base, runtime-validated).CXWnd::pFirstChild+0x10(TList base, runtime-validated).CXWnd::dShow+0x196(slot 68/69 ICF body —IsVisible() && !IsMinimized()).CXWnd::Minimized+0x1CE(free byproduct of slot 68/69).CSidlManagerBase::XMLDataMgr+0x144(CXMLDataManager-base offset within the containedCXMLParamManager).
- iter-12's MQ2-style walks invoke
IterateAllWindowsPublicfromLoginStateMachine::Tick(via theLoginController::GiveTimedetour), which runs on EQ's game thread. That same thread servicesIDirectInputDevice8::GetDeviceState, the path delivering SHM-injected BURST keystrokes. With the toggle on, the background client'sGetDeviceStatepolling stalled while the walk was in flight, dropping password keystrokes. Foreground client has a Win32 keyboard fallback path that bypassesGetDeviceState, so it landed clean — hence deterministic foreground-OK / background-fail. Confirmed by 4 dual-box test runs and 3 independent code-review agents at 75% confidence. Toggle off restores v3.12.0 behavior. - Future v6 design (Combo G primary): direct memory write to
InputTextCXStr at+0x1A8, skip BURST keystrokes entirely. Foundation laid by these pinned offsets + the dormant MQ2-style code.
- Wait
phase >= ClickingConnect(wasWaitConnectResponse) — Dalaya advances to ClickingConnect ~2s after SendLoginCommand; the broken Connect button never advances to WaitConnectResponse, so the previous gate spent the full 15s timeout for nothing. Cuts ~13s off the dual-box happy path. - New
WarmupDwellMsconfig (default 4000ms) replaces the flat 5sLoginScreenDelayMsin the SHM-warmup path.LoginScreenDelayMsis kept as a fallback only. CombinedTypeStringper-character timing tightened — 80ms→25ms, 50ms→15ms, shift-up 40ms→15ms. A 6-char password now takes ~240ms total (was ~780ms) — paste-like at 60fps.FireTeamhonorsClient Launch Delay(Settings → Video → Client Launch Delay, default 1s) between team slots. Previously the autologin team-fire path calledLoginAndEnterWorldin a tight loop with 0ms gap, racing Dalaya's auth gate when concurrent BURST 1 submits landed within ~30ms.
- Extracted
RunCredentialEntryfromRunLoginSequence— single method with honest docs: warmup → dwell → BURST 1 → cancel. Removes the dualshmDidCredentialsdance. - New
LoginCredentialsSentevent fires after BURST 1 deactivate.TrayManagernow applies slim-titlebar + hook config + window title at T+~7s instead of T+~30s (was waiting on charselect-ready).LoginCompleteis kept as the idempotent end-of-sequence; both call the sharedApplyDeferredCosmetics(pid). SendCancelCommandfires BEFORE BURST 1 (was AFTER for one mid-iteration that caused truncation — the DLL'sPHASE_CLICKING_CONNECTloop was pollingMQ2Bridge::ClickButtonconcurrent with typing, contending for EQ's message pump).
- Sync context late-bind —
TrayManager.Initialize()now installs the WinFormsSynchronizationContextpost-NotifyIcon and propagates it toAutoLoginManagervia a newSetUiContext(). Previously_syncContextcaptured pre-Application.Runwas null; events fell into the synchronous-fire branch on background threads, racingTrayManager._injectedPidsand other UI state. FireTeamShowWarning marshal —ShowWarning → DeferToNextTick → WinForms.Timerconstruction MUST happen on the UI thread. Now wrapped in_uiContext.Postinside theTask.Runlambda. Previously silently broken (timer never ticked) when no team slots were assigned andFireTeamwas running on the threadpool.
- Clean Release build + dual-box smoke 2026-04-25 ("SLICKED!!!").
- Combo G CStrRep_Dalaya layout corrected (
Native/eqmain_cxstr.h) — utf8 verified at +0x14 via runtime hex dump; introducedownerPtrfield at +0x10 to document the eqmain-internal pointer that lives there. Live recon supersedes the 2013 disassembly comment. WriteEditTextDirectread-back verification (Native/eqmain_cxstr.cpp) — afterConstructFromCStrsucceeds, the written CStrRep'slengthand first utf8 byte are verified against what was requested. Returns false (callers fall back to keystroke) on any mismatch. Caught a real silent-success bug where the function reported success while writing into the wrong widget memory.PHASE_CLICKING_CONNECTvtable gate (Native/login_state_machine.cpp) —MQ2Bridge::FindWindowByNamereturns a CXMLDataPtr def (vtable = eqmain DOS header) when no liveLOGIN_ConnectButtonwidget exists. Pre-fix, the DLL calledMQ2Bridge::ClickButtonon the def, which silently early-returned, and the state machine advanced phase regardless. Now gated onEQMainOffsets::IsEQMainButtonWidget; if not a realCButtonWnd, retry up to 50 times thenSetErrorso C# falls back loudly. Counter resets inInvalidateWidgetsso a fresh login attempt starts clean.- C# SHM credentials warmup ritual (
Core/AutoLoginManager.cs::RunLoginSequence) — sendsLOGINSHM command and waits up to 15s forphase >= WaitConnectResponse. On Dalaya phase never advances pastClickingConnect(no live button), so the 15s timeout always fires — but the DLL's widget-discovery activity during that window warms up EQ's input subsystem so BURST 1 keystrokes land cleanly. Then BURST 1 runs unconditionally. g_passwordredacted from DLL log (Native/eqmain_cxstr.cpp) —WriteEditTextDirectnow logstextLen=N+ first-byte hex only, never the full string. Earlier diagnostic loggedtext="Exodus"(real password) into the DI8 log file.
- Autologin landed at ~6s wait → in-world per dual-box test 2026-04-25. (Was timeout-bound around 35-50s previously.)
- Combo G writes to
+0x1A8 InputTextsuccessfully, but EQ renders/submits from a different buffer — direct SHM password injection still doesn't work end-to-end on Dalaya. BURST 1 keystrokes are the actual workhorse. - Two parallel autologin paths (SHM warmup ritual + BURST 1 keystrokes) is confusing; warmup needs to be repurposed or replaced with a non-credential-attempt mechanism.
- Stale
LoginShm overall timeout (14s)log message corrected to(45s)— the timeout was bumped to 45s in iter 15.2 but the message text was never updated, leading to false impressions when reading logs. - Stale
// PATH A: ... DISABLED — native widget discovery needs a dedicated RE session.comment block replaced with current reality + a ⚠ LOAD-BEARING SIDE EFFECT ⚠ warning. Combo G fixed widget discovery; the broken piece is now the DLL's post-connect detection. PATH A's 45s timeout, although the "intended" login flow never completes, is incidentally serving as the warmup that PATH B's keystroke injection requires — without it, BURST 1 fires at T+10s and EQ drops the first ~3 keystrokes (verified 2026-04-25 by attempting C# disable, password truncated 6→3 chars, login failed, rolled back).
- No behavior change vs v3.11.1 — only comments and one log message.
- The "skip PATH A entirely" win identified during analysis turned out to need a non-time-based BURST-1 readiness gate, not just commenting-out the if-block. Tracked as a future "D" task.
\(SwitchKey) was firing globally — any press in chat, Discord, browsers, etc. was being swallowed by the keyboard hook. Now scoped to "EQ client window must be foreground" via the existingprocessFilterpath.](GlobalSwitchKey) remains genuinely global, as designed.- Removed the cold-start "no EQ focused → focus first client" branch from the primary path of
OnSwitchKey(left in as a defensive no-op). The previous EQ-only filter had been temporarily removed on 2026-04-24 to work around a broken-autologin foreground race; that race is gone now that autologin lands EQ as foreground end-to-end.
- License changed from GPL-3.0 to GPL-2.0-or-later — ecosystem alignment with MacroQuest (MQ2) and the broader EverQuest tool community, which is uniformly GPLv2-only. GPL-2.0-or-later is upward-compatible with GPLv3 for anyone who wants it, while unlocking legitimate code-sharing with MQ2-derived work. Prior releases (v3.9.3 and earlier) remain GPL-3.0 forever. Tag
v3.9.3-last-gplv3marks the relicense boundary. - README License section — formal attributions added for Stonemite (DirectInput proxy approach studied, no code taken) and MacroQuest (character-select facts referenced, no source compiled in). SHM boundary between EQSwitch and MQ2 DLL reaffirmed.
- README fan-made disclaimer strengthened — EQSwitch is free, educational, independent, and not sold.
- CONTRIBUTING.md — contributor license grant updated to GPL-2.0-or-later.
- GiveTime detour replaces SetTimer-based polling — login state machine now rides the game's own game-loop tick (50–130 Hz), matching the MQ2 pattern for stable, high-frequency polling.
LoginController*fast-path — when the controller pointer is already resolved, subsequent logins skip the full scan.- Charselect robustness — detects
eqmain.dllunload at character select and resumes the background poll instead of bailing. LoginShmWriterwired into the native path.
- MQ2-style
eqmain.dlldetection with widget-ownership tracking. - Corrected
SetWindowTextvtable slot + exact-vtable class gate for login-widget match. HeapScanForWidget— locates login widgets by SIDL name on the heap.- Live
CXWnddiscovery: tree walk, heap cross-reference, label search. - Improved
CXWndManagerdiagnostics.
Sole-author relicensing (verified via git log). No external contributors held copyright on any EQSwitch code. No user-visible behavior change from the relicense itself.
- Version bump for public release following v3.9.2 security hardening.
- SHM-driven enter-world — in-process
CLW_EnterWorldButtonclick via shared-memory request/ack handshake (replaces earlier PostMessage approach for this step). - Charselect slot probe — runtime validation that the resolved slot matches the user's intended character before commit.
- Vtable guard around
GetChildItem— defensive check before calling into MQ2-exported thunks.
- WinGet-compatible distribution — packaging adjustments for smooth WinGet manifest submission.
- Security hardening for distribution — installer / update path review, string scrubbing, no secrets in binaries.
- Log-spam reduction in native
NetDebugoutput during charselect polling.
- Per-account and per-team
AutoEnterWorldflag — granular control over which accounts auto-commit to character select vs. stop at the character screen. - DLL verification report (
Native/VERIFICATION.md) — independent reverse-engineering evidence for MQ2 export offsets used on Dalaya. - Volatile cross-thread fields in native login state machine (C++ memory-model correctness fix surfaced by 9-agent audit that fixed 8 bugs).
LaunchTwo→LaunchAllterminology cleanup. Config migrated v2 → v3 (duplicateLaunchTworemoved).
- Log trimming — async stream-based trimmer with archive to
Logs/archive/. Default threshold 50 MB; configurable. - Account backup / import — DPAPI blobs portable across imports on the same Windows user.
- Team submenu rework —
LaunchTwofor bare clients, Launch Team restored, all 4 teams in tray submenu.
- Hotkeys / Video / Accounts tabs — card padding, conflict warnings, Windowed Mode relocated to Window Style card.
- Native vtable validation before
GetChildItemcall (prevents crash on Dalaya's variant EQ client). - Retry counter for charselect window search with bounded cap (was unbounded busy-wait).
- Lazy MQ2 init — replaces blocking
Sleep(2000)startup delay. - MemoryBarrier before SHM charCount write — ordering fix for cross-thread reads.
- Background auto-login works end-to-end while EQ is unfocused. Root cause was an inline
GetForegroundWindowhook iniat_hook.cppthat only spoofed for callers withineqgame.exe's address range — EQ's game loop calls from loaded DLLs fell outside that range. Three-layer fix:- Inline hooks skip the caller check when SHM is active, so
GetForegroundWindow/GetFocus/GetActiveWindowall return EQ's HWND. - Persistent WndProc subclass blocks
WM_ACTIVATEAPP(FALSE)/WM_ACTIVATE(WA_INACTIVE)/WM_KILLFOCUS/WM_NCACTIVATEwith a 16 ms re-install timer. - Activation blast on re-install after EQ's 3D char select overwrites the subclass.
- Inline hooks skip the caller check when SHM is active, so
- Unconditional 200 ms re-post of
WM_ACTIVATEAPP(1)while SHM active (old self-check was defeated by the hook's own spoofing). CallWindowProcA→CallWindowProcWfor Unicode compatibility.
- Replaced
dinput8.dllproxy with CREATE_SUSPENDED process injection. EQSwitch now injectseqswitch-di8.dllandeqswitch-hook.dlldirectly intoeqgame.exeafter resuming the loader (~50 ms). Dalaya's 1.3 MB MQ2dinput8.dllstays untouched — no patcher conflicts, no server hash validation failures.
- Character select Enter World uses 250 ms key holds with 3 retry attempts and real title-change verification.
ActivateThreadcontinuously re-postsWM_ACTIVATEAPP(1)while SHM active to defend against focus loss.- Adaptive
WaitForScreenTransition— replaces fixed 3 s post-server-select sleep; pollsIsHungAppWindow+GetWindowRectstability and handles any load time (5 – 90 s).
- Dead proxy files (prior
dinput8.dllproxy architecture).
- Self-updater handles all shipped files —
EQSwitch.exe,eqswitch-hook.dll, anddinput8.dll(previously misseddinput8.dll). --test-updateCLI flag for simulating full update flow locally without a GitHub release.- Post-update toast notification on relaunch.
ConfigVersionMigrator— versioned framework that transforms raw JSON before deserialization; preserves user settings across property renames and type changes.
- Retry logic for
.oldartifact cleanup (race with memory-mapped exe). - CTS dispose race in
UpdateDialog.
- Removed broken framework-dependent build from CI — Release artifacts are self-contained single-file only.
- Native DLLs bundled into release zip —
eqswitch-hook.dll(and latereqswitch-di8.dll) ship alongside the exe.
- AppConfig defaults baselined to EQ's eqclient.ini — nuclear reset now matches a fresh EQ install (22 booleans, 3 clip planes, mouse sensitivity, sound volume)
- INI section targeting corrected — ChatSpam writes to [Options] not [Defaults], Keymaps writes to [KeyMaps] not [Defaults], Particles routes FogScale/LODBias/SameResolution to [Options]
- 11 phantom [Defaults] writes removed — Sky, BardSongs, Anonymous, ClipPlane, MouseSensitivity, ShadowClipPlane, ActorClipPlane and 4 others were being injected into [Defaults] where they don't belong; now write only to [Options]
- LoadFromIni reads [Options] section — settings that live in [Options] (EQ's runtime-authoritative section) are now read correctly on form open
- SlowSkyUpdates EnforceOverrides — now restores EQ default (3000ms) when unchecked instead of leaving 60000
- SkyUpdateInterval ApplyToIni — falls back to 3000 when no original value was captured
- MouseSensitivity/SoundVolume LoadFromIni clamp — minimum changed from 0 to -1 to preserve sentinel values
- ForceWindowedMode — now reads from [Defaults] in addition to [VideoMode]
- DisableEQLog — moved from AppConfig to EQClientIniConfig; LoadFromIni now reads Log key; ApplyToIni now writes it
- ConfiguredKeys sentinel tracking — numeric fields at sentinel values (-1 or 0) are removed from ConfiguredKeys instead of being tracked but never enforced
- Maximized ConfiguredKeys gap — now tracked when saved from SettingsForm
- ProcessManagerForm FPS ConfiguredKeys — MaxFPS/MaxBGFPS now tracked when saved from Process Manager
- ChatSpamForm EnforceOverrides — safe int serialization (
value != 0 ? "1" : "0") instead of rawvalue.ToString() - ModelsForm phantom writes on load failure — _initialValues snapshot moved outside try block
- Snapshot early-return bypass — all 4 sub-forms restructured from
if (!exists) returntoif (exists) { try...catch }so snapshots run unconditionally - VideoModeForm XOffset/YOffset defaults — changed from 1 to 0 (EQ default)
- GDI font leak coverage — added DisposeControlFonts to EQChatSpamForm, EQVideoModeForm, EQClientSettingsForm, FirstRunDialog, ProcessManagerForm, SettingsForm
- ChatServerPort writes to [Options] (was [Defaults], key doesn't exist in fresh ini)
- Doc comments updated to accurately describe EQ defaults and section locations
-
DirectInput proxy DLL (
Native/dinput8.dll) — IAT hook proxy that interceptsGetForegroundWindow,GetAsyncKeyState, andGetKeyboardStateinside eqgame.exe. Per-PID shared memory injects scan codes into EQ's DirectInput keyboard device without stealing focus. -
Background auto-login — Types passwords into background EQ windows via DirectInput shared memory injection. True one-click multi-account login with no focus stealing.
-
SetWindowTextA hook — Custom window titles now persist through zone transitions, login, and character select. The injected DLL intercepts EQ's own SetWindowTextA calls and substitutes the configured title. Same approach as WinEQ2.
-
ShowWindow hook — Blocks EQ from minimizing itself on focus loss during DirectX init. Fixes the Maximize-on-Launch + no-Slim-Titlebar crash where EQ would get stuck minimized.
-
Auto hook injection — Hook DLL now injects whenever any hook feature is needed (custom window title, maximize protection, or slim titlebar), not just when slim titlebar + hook is toggled on.
-
Video Settings description — Added page description and "Monitor Selection" section title for clarity.
-
Resolution hint — Yellow hint in Window Style card when slim titlebar is disabled, reminding to set EQ resolution to fit above the taskbar.
-
Help form auto-login section — Documents background login status and dinput8.dll requirement.
- Auto-login typing — Switched from VK+scancode to KEYEVENTF_UNICODE for reliable text entry on EQ's login screen. FocusAndSendKey re-focuses before each keystroke to survive focus theft.
- Hook injection during login — DLL injection and slim titlebar guard are deferred until login sequence completes, preventing focus theft mid-login.
- Window title not applied on discovery — Titles now appear immediately when EQ is detected, not just during explicit arrange operations.
- Build: TestInput sub-project conflict — Excluded TestInput/ from default compile globbing to prevent duplicate assembly attribute errors.
- Hook DLL shared memory struct extended with
blockMinimizeflag and 256-bytewindowTitlebuffer (284 bytes total, up from 24). - License changed to GPL-3.0 — Matches Stonemite's license (studied their DirectInput proxy approach).
- Per-process shared memory — Each injected eqgame.exe gets its own memory-mapped file (
EQSwitchHookCfg_{PID}) instead of a single global mapping. Hook DLL now works in both single and multimonitor modes with correct per-window positioning. - Atomic config writes —
ConfigManager.FlushSavewrites to a temp file thenFile.Moveto prevent config corruption on crash.
- Hook configs not updated after ArrangeWindows/ToggleMultiMonitor — Hook DLL would snap windows back to stale positions after "Fix Windows" or mode toggle.
- Hook configs not updated after SwapWindows — Multimonitor swap would be immediately undone by the hook.
- DllInjector handle leaks —
hThreadhandles in bothInject()andEject()moved intofinallyblocks. - DllInjector.Eject dead code — Removed unused
allocAddr/VirtualFreeExand staleResolveLoadLibraryAcall. - GetExportRva missing guards — Added
-1checks afterRvaToFileOffsetcalls for clearer PE parse errors. - HookConfigWriter resource leak —
Open()catch path now disposes bothMemoryMappedFileandViewAccessoron failure. - HookConfigWriter.Disable zeroed geometry — Now read-modify-writes to only flip the
Enabledflag. - Dead-PID injection race — Timer tick guards against injecting into a process that died during the 2s delay.
- Missing client early-return —
UpdateHookConfigForPidlogs and returns when PID not in client list. - Stream leak in LoadIcon —
GetManifestResourceStreamnow disposed withusing. - SettingsForm font leaks — Inline fonts on labels and DataGridView header tracked and disposed.
- Double-dispose of foreground debounce timer — Removed redundant dispose in
TrayManager.Dispose. - PID naming contract — Cast to
uintfor shared memory name to match C++%luformatting.
- DLL hook injection (
Core/DllInjector.cs,Native/eqswitch-hook.dll) — Injects a native MinHook-based DLL into eqgame.exe that hooksSetWindowPosandMoveWindow. Enforces window position/style via shared memory-mapped config (HookConfigWriter.cs). Prevents EQ from fighting window management. - DPAPI-encrypted auto-login (
Core/AutoLoginManager.cs,Core/CredentialManager.cs) — Account presets with username, encrypted password, server, character name, and slot. Full enter-world automation viaSendInputon a background thread. Credentials encrypted withDataProtectionScope.CurrentUser— only the same Windows user on the same machine can decrypt. - Login Accounts model (
Models/LoginAccount.cs) — Stored account presets for auto-login with name, username, encrypted password, server, character, slot, and login flag toggle. - PiP orientation support — PiP overlays adapt to window orientation and layout changes.
- Hook config shared memory (
Core/HookConfigWriter.cs) — Memory-mapped file (EQSwitchHookCfg) shared between C# host and injected DLL. Struct-matched layout (packed, sequential ints) for target position, style, and enable flag. - Native hook source (
Native/) — Full MinHook source (buffer, trampoline, HDE32/64 disassembler) pluseqswitch-hook.cppwith build scripts for MSVC and MinGW.
- Settings expanded — New Auto-Login tab with account management, credential encryption, and launch integration.
- PipOverlay enhanced — 87 lines added for orientation-aware thumbnail rendering.
- TrayManager expanded (982 → 1549 lines) — Auto-login menu integration, DLL injection lifecycle, hook config management.
- SettingsForm expanded (734 → 1211 lines) — Auto-login account editor, DLL hook controls, PiP orientation settings.
- README updated with new feature descriptions.
- Unit test project (
EQSwitch.Tests/) — Removed during architecture transition. Tests covered v2.x patterns that no longer apply post-DLL injection. - Solution file — Simplified to single-project build.
- PLAN_DLL_HOOK.md — Planning doc removed after implementation.
- Tray clicks simplified — Removed triple-click entirely. Left button: single + double click. Middle button: single + triple (via click counting —
MouseDoubleClickdoesn't fire for middle onNotifyIcon). - Launch is bare-bones — Removed
EnforceOverrides,EnforceWindowedModeIfBorderless,PositionOnTargetMonitor, and post-launchArrangeWindows. Launch just startseqgame.exewith staggered delay. Added restore-if-minimized after 3s. - Settings UI cleanup — Removed CtrlHoverHelp (unreliable in overflow tray). Human-readable switch mode labels ("Swap Last" / "Cycle All"). Tray Click Actions card redesigned. Preferences card alignment fixed.
- Paths tab auto-open — Clicking GINA or Dalaya Patcher in launcher menu opens Settings → Paths tab if path not set.
- Tooltip Delay — Renamed, supports 0 = disabled.
- Multi-Monitor Mode checkbox in Video Settings synced with config.
- eqclient.ini corruption —
EnforceOverrideswas writingMaximized=1and offsets=-8 on every launch, causing windows to minimize. Removed from launch path entirely. - Hotkeys tab overlapping labels in Actions card — clean 2-column grid.
- Multi-monitor video settings — Monitor picker, per-monitor resolution, position preview.
- Config backup restore — Restore from any of the 10 backup rotations.
- Tabs consolidated — Merged Performance + Launch into Hotkeys tab. Reduced from 8 to 6 tabs.
- Stacked fullscreen as default layout — Clients stack on top of each other, arranged in stacked mode.
- FPS writes to [Options] section — Correct INI section for MaxFPS/MaxBGFPS.
- Priority default changed to AboveNormal (was High).
- Process Manager redesigned — Priority card moved to top, CPU thread mapping card, grid refresh paused during edits.
- Video Settings overhaul — Reordered submenu, preset sizes fixed.
- DefaultFont crash — Null reference on systems without default font.
- Launch positioning — Don't force window offsets on every launch, respect user INI edits.
- PiP anchor — Fixed anchor point for overlay positioning.
- Dalaya patcher path handling.
- Direct switch hotkeys (Alt+1-6) disabled by default to avoid conflicts.
- Hotkey conflict warning appearing on every Settings close.
- PiP max windows label layout and custom size capped to 960×720.
- Swap Windows feature — removed (stacked mode replaces it).
- CharacterEditDialog — removed (per-character overrides simplified).
- Slim titlebar mode (WinEQ2 style) — Strips
WS_THICKFRAME(resize border) while keepingWS_CAPTION(thin title bar). Positions window at fullrcMonitorbounds to overlap taskbar. Replaces both "borderless" and "remove title bars" options with a single unified mode. - Auto-apply slim titlebar — Guard timer re-applies style when EQ fights the window decoration changes.
- EQClientSettingsForm expanded — Additional eqclient.ini toggle controls.
- WindowManager rewritten (280+ lines changed) — Unified slim titlebar logic, monitor bounds calculation, style manipulation.
- Settings Layout tab — Slim titlebar checkbox replaces borderless + remove-title-bar checkboxes.
- LaunchManager simplified — Removed post-launch window positioning (slim titlebar handles it).
- ROADMAP.md — Removed from project (tracked in root
Roadmap_master.md). - Borderless fullscreen mode — Superseded by slim titlebar mode.
- Remove Title Bars option — Superseded by slim titlebar mode.
- Consolidated Process Manager — 3 clear cards: Windows Priority, Core Assignment, FPS Limits
- INI-based Core Assignment — 6 NumericUpDown slot pickers for CPUAffinity0-5, writes directly to eqclient.ini
- Ghost FPS label — shows current eqclient.ini MaxFPS/MaxBGFPS values alongside the editor
- FPS defaults changed to 80/80 (was 0 = unlimited, which crashes EQ)
- Priority defaults changed to High/High (prevents virtual desktop crashes + enables autofollow)
- Settings "Affinity" tab renamed to "Performance" — stripped to enable toggle + retry settings
- Submenu directions — Video Settings, Settings, Launcher all open upward (AboveRight)
- CharacterEditDialog simplified — priority override only (core assignment now global via eqclient.ini)
- ThrottleManager — process suspension was causing "Suspended" in Task Manager
- CPU Affinity submenu from tray menu — Process Manager is the one-stop shop
- Per-character AffinityOverride — replaced by global eqclient.ini CPUAffinity0-5 slots
- Per-character CPU affinity — assign different core masks to individual characters. Characters with
AffinityOverrideuse their custom mask instead of the global active/background masks. - Per-character process priority — set individual characters to Normal, AboveNormal, or High priority. Characters with
PriorityOverrideuse their custom priority instead of the global setting. - Character Edit dialog — double-click a character in Settings → Characters tab to edit affinity mask and priority overrides. Checkbox toggles override on/off, hex mask input with validation.
- Process Manager "Source" column — shows "Custom" (cyan) for clients using per-character overrides, "Global" for clients using default settings.
- Reset Defaults button in Video Settings form — resets Width/Height (1920×1080), offsets (0,0), Windowed Mode (on), Disable Log (off), Title Bar Offset (0). Matches AHK v2.4
ResetVMDefaults. Requires Save or Apply to write to disk.
- Configurable tray click actions — Settings → General tab lets users bind single/double/triple/middle-click to specific actions (Launch One, Fix Windows, Open Settings, etc.)
- Custom video presets — Save up to 3 custom resolutions in Video Settings (FIFO eviction, duplicates skipped)
- Dark-themed context menus —
DarkMenuRendererapplies dark background/foreground to all tray menu items - FloatingTooltip — replaces
MessageBox.Show"already running" popup with a non-blocking floating tooltip
- Tray context menu reorganized into grouped submenus (Video Settings, Settings, Launcher)
- Medieval emoji/icon prefixes restored on all tray menu items (matches AHK v2.4 style)
- CPU Affinity submenu simplified — removed per-core checkboxes, shows info labels only
- Process Manager restyled with dark DataGridView theme
- First-run now auto-opens Settings instead of requiring manual navigation
- Background FPS throttling (
Core/ThrottleManager.cs) — duty-cycles background EQ clients viaNtSuspendProcess/NtResumeProcess. Configurable throttle percent (0-90%) and cycle interval. Active client is never throttled. Settings on Affinity tab. - Borderless fullscreen mode — WinEQ Y+1 offset trick: strips window decorations and positions at
(monitor.Left, monitor.Top+1)usingrcMonitorbounds. Preserves Alt+Tab and PiP window overlay. Checkbox on Layout tab.
- Persistent file logging (
Core/FileLogger.cs): Info/Warn/Error with timestamp, 1MB rotation, thread-safe - Input validation (
AppConfig.Validate()): Clamps all numeric config fields to safe ranges on load and save - IWindowsApi interface (
Core/IWindowsApi.cs): Abstraction layer for Win32 calls, enables unit testing - 79 unit tests across 7 test files: AppConfig, WindowManager, AffinityManager, ConfigManager, ConfigMigration, HotkeyManager, EQClient
- Solution file (
EQSwitch.sln): Main project + xUnit test project with Moq
- P2-02: Context menu client labels now update when window titles change
- Concurrency: KeyboardHookManager uses
ImmutableHashSet<int>(lock-free) instead ofHashSet<int>with lock - Concurrency: ProcessManager fires events outside lock block, uses specific exception catches
- Concurrency: AffinityManager snapshots retry counters before iterating
- Resource leak: LaunchManager implements IDisposable, cancels launches on dispose
- Resource leak: ProcessManagerForm stops refresh timer in Dispose()
- Backup pruning: ConfigManager sorts by file write time instead of filename string
- Hotkey ID overflow:
_nextIdresets to 1 onUnregisterAll()(P4-01) - Exception hardening: DWM HRESULT mapped to readable messages in PipOverlay
- Exception hardening: VideoSettingsForm retries file I/O (2x, 500ms)
- Magic numbers: Named constants replace all magic numbers across 6 files
- All diagnostic logging migrated from
Debug.WriteLinetoFileLogger - WindowManager and AffinityManager accept optional
IWindowsApifor dependency injection
- P0-01: Hook callback dispatched async via SynchronizationContext.Post() — prevents Windows killing the LL hook on slow callbacks
- P0-02: Global switch key
]no longer swallowed when zero EQ clients running (requireClients guard + cached PID check) - P0-03: PiP overlay Ctrl+drag works — replaced WS_EX_TRANSPARENT with dynamic WM_NCHITTEST (HTTRANSPARENT default, HTCLIENT when Ctrl held)
- P0-04: eqclient.ini read/write uses ANSI encoding instead of UTF-8 to prevent corruption
- P1-01: Eliminated Process.GetProcessById() in hook callback — cached PID HashSet with GetWindowThreadProcessId
- P1-02: Screen.PrimaryScreen null-safe fallback for headless/RDP disconnect
- P1-03: All eqclient.ini file operations use Encoding.Default consistently
- P1-04: Triple-click tray detection resets timestamp on every click
- P1-05: Run-at-startup registry path validated on launch — auto-corrects if exe moved
- P1-06: LaunchManager timers cancelled on config reload and shutdown
- P1-07: Minimum 500ms enforced between staggered launches
- P1-08: ContextMenuStrip disposed in Shutdown path
- P1-09: Previous custom icon disposed on LoadIcon reload
- Process Manager GUI: Live view of all EQ clients with PID, character name, priority, and affinity mask. Auto-refreshes every second. Includes Force Apply button.
- PiP Settings tab: Configure PiP size preset, custom dimensions, opacity, border color, and max windows from Settings GUI
- Characters tab: View character profiles with Export/Import buttons for JSON backup
- All Cores / Clear buttons: Quick-select on Affinity tab to set masks to system max or minimum
- Force Apply Affinity: Tray menu item to re-apply affinity rules to all clients immediately
- Triple-click tray: Triple-click the tray icon within 500ms to arrange all windows
- Desktop shortcut: Create Desktop Shortcut menu item via WScript.Shell COM
- "Process Info" balloon replaced with full Process Manager window
- PiP config now persists through Settings GUI (was only configurable via JSON)
- ReloadConfig now includes PiP settings for hot-reload
Complete rewrite from AutoHotkey v2 to C# (.NET 8 WinForms).
- Settings GUI: 6-tab dark-themed settings dialog (General, Hotkeys, Layout, Affinity, Launch, Paths)
- Video Settings: Read/write eqclient.ini [VideoMode] section with resolution presets
- PiP Overlay: DWM thumbnail-based live previews of background EQ clients
- Click-through overlay (won't steal focus)
- Ctrl+drag repositioning with position persistence
- Auto-hide when fewer than 2 clients
- Window Swap: Rotate window positions (1→2, 2→3, N→1)
- Hung window detection: Skip unresponsive windows during arrange/swap operations
- Files submenu: Quick access to log files, eqclient.ini, GINA, notes
- Links submenu: Dalaya Wiki, Shards Wiki, Fomelo
- Help window: Full hotkey reference and feature guide
- Run at Startup: Registry-based toggle in tray menu
- Tray interactions: Double-click to launch, middle-click for PiP
- Hotkey suffixes: Keyboard shortcuts shown in tray menu items
- Config migration: Auto-import from AHK eqswitch.cfg on first run
- Config format: INI → JSON (eqswitch-config.json)
- Config backups: automatic rotation (keeps last 10)
- Hotkey system: RegisterHotKey + WH_KEYBOARD_LL (was AHK native)
- Window arrangement: proper multi-monitor support via EnumDisplayMonitors
- CPU affinity: configurable retry on launch (EQ resets affinity after startup)
- Build output: .NET single-file publish (no AV false positives)
- AutoHotkey dependency
- Flash suppress / auto-minimize (superseded by PiP + affinity management)