fix(client): restore retail interaction parity
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -8,10 +8,9 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// debug bindings that have no retail equivalent.
|
||||
///
|
||||
/// <para>
|
||||
/// K.1a defined the enum and K.1c flipped the bindings table to the full
|
||||
/// retail preset. Runtime controllers subscribe by subsystem; actions whose
|
||||
/// owning panel has not landed yet (for example <c>UseSpellSlot_*</c>) may
|
||||
/// intentionally remain undispatched.
|
||||
/// The installed Sept-2013 ActionMap's 306 user-bindable rows each have one
|
||||
/// distinct enum identity and one live subsystem consumer. Low, non-bindable
|
||||
/// MasterInputMap commands remain separate infrastructure actions.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public enum InputAction
|
||||
|
|
@ -92,7 +91,10 @@ public enum InputAction
|
|||
// ── UICommands ────────────────────────────────────────
|
||||
/// <summary>Use the selected item / interact (retail R).</summary>
|
||||
UseSelected,
|
||||
/// <summary>Cancel the topmost UI / clear selection / open log-out menu.</summary>
|
||||
/// <summary>
|
||||
/// Retail Escape priority: cancel focused UI/targeting/movement, clear
|
||||
/// selection, then toggle the Gameplay Options page.
|
||||
/// </summary>
|
||||
EscapeKey,
|
||||
/// <summary>Log out of the game (retail Shift+Esc).</summary>
|
||||
LOGOUT,
|
||||
|
|
@ -169,7 +171,7 @@ public enum InputAction
|
|||
// ── Combat ────────────────────────────────────────────
|
||||
/// <summary>Toggle combat-stance on/off (retail Grave / `).</summary>
|
||||
CombatToggleCombat,
|
||||
// Mode-dependent (dormant in K — Phase L lights them up)
|
||||
// Mode-dependent retail combat actions.
|
||||
CombatDecreaseAttackPower,
|
||||
CombatIncreaseAttackPower,
|
||||
CombatLowAttack,
|
||||
|
|
@ -267,7 +269,8 @@ public enum InputAction
|
|||
AcdreamToggleAudioMute,
|
||||
/// <summary>F (existing) toggles between fly camera and orbit/chase mode.</summary>
|
||||
AcdreamToggleFlyMode,
|
||||
/// <summary>Tab — currently toggles fly↔player mode (will be reassigned to ToggleChatEntry in K.1c).</summary>
|
||||
/// <summary>Legacy acdream player-mode toggle. Intentionally unbound;
|
||||
/// retail Tab is <see cref="InputAction.ToggleChatEntry"/>.</summary>
|
||||
AcdreamTogglePlayerMode,
|
||||
/// <summary>Hold-RMB chase-camera orbit (debug-only, not user-rebindable).
|
||||
/// Camera orbits around the player while held; never drives character yaw.</summary>
|
||||
|
|
@ -285,4 +288,201 @@ public enum InputAction
|
|||
CameraRaise,
|
||||
/// <summary>Camera lower (held key, integrates Pitch−= adjSpeed·dt·0.02). Default unbound.</summary>
|
||||
CameraLower,
|
||||
|
||||
// ── Remaining Sept-2013 retail ActionMap identities ────────────
|
||||
// Appended after every pre-existing member so persisted numeric enum values
|
||||
// remain stable. These are distinct even where retail reuses the same
|
||||
// Action id in another InputMap (notably CameraAlternateControls).
|
||||
|
||||
CameraAlternateMoveToward,
|
||||
CameraAlternateMoveAway,
|
||||
CameraAlternateRotateLeft,
|
||||
CameraAlternateRotateRight,
|
||||
CameraAlternateRotateUp,
|
||||
CameraAlternateRotateDown,
|
||||
CameraAlternateViewDefault,
|
||||
CameraAlternateViewFirstPerson,
|
||||
CameraAlternateViewLookDown,
|
||||
CameraAlternateViewMapMode,
|
||||
|
||||
UseSpellSlot_10,
|
||||
UseSpellSlot_11,
|
||||
UseSpellSlot_12,
|
||||
|
||||
EmoteAfkState,
|
||||
EmoteAkimbo,
|
||||
EmoteAToyotState,
|
||||
EmoteAkimboState,
|
||||
EmoteAtEaseState,
|
||||
EmoteBeckon,
|
||||
EmoteBeSeeingYou,
|
||||
EmoteBlowKiss,
|
||||
EmoteBowDeep,
|
||||
EmoteBowDeepState,
|
||||
EmoteClapHands,
|
||||
EmoteClapHandsState,
|
||||
EmoteCringe,
|
||||
EmoteCrossArmsState,
|
||||
EmoteCurtseyState,
|
||||
EmoteDrudgeDance,
|
||||
EmoteDrudgeDanceState,
|
||||
EmoteHaveASeat,
|
||||
EmoteHaveASeatState,
|
||||
EmoteHeartyLaugh,
|
||||
EmoteHelper,
|
||||
EmoteKneel,
|
||||
EmoteKneelState,
|
||||
EmoteKnock,
|
||||
EmoteLeanState,
|
||||
EmoteMeditateState,
|
||||
EmoteMimeDrinking,
|
||||
EmoteMimeEating,
|
||||
EmoteMock,
|
||||
EmoteNod,
|
||||
EmoteNudgeLeft,
|
||||
EmoteNudgeRight,
|
||||
EmotePlead,
|
||||
EmotePleadState,
|
||||
EmotePoint,
|
||||
EmotePointDown,
|
||||
EmotePointDownState,
|
||||
EmotePointLeft,
|
||||
EmotePointLeftState,
|
||||
EmotePointRight,
|
||||
EmotePointRightState,
|
||||
EmotePossumState,
|
||||
EmotePray,
|
||||
EmotePrayState,
|
||||
EmoteReadState,
|
||||
EmoteSalute,
|
||||
EmoteSaluteState,
|
||||
EmoteScanHorizon,
|
||||
EmoteScratchHead,
|
||||
EmoteScratchHeadState,
|
||||
EmoteShakeFist,
|
||||
EmoteShakeFistState,
|
||||
EmoteShakeHead,
|
||||
EmoteShiver,
|
||||
EmoteShiverState,
|
||||
EmoteShoo,
|
||||
EmoteShrug,
|
||||
EmoteSitState,
|
||||
EmoteSitBackState,
|
||||
EmoteSitCrossleggedState,
|
||||
EmoteSlouch,
|
||||
EmoteSlouchState,
|
||||
EmoteSmackHead,
|
||||
EmoteSnowAngelState,
|
||||
EmoteSpit,
|
||||
EmoteSurrender,
|
||||
EmoteSurrenderState,
|
||||
EmoteTalkToTheHandState,
|
||||
EmoteTapFoot,
|
||||
EmoteTapFootState,
|
||||
EmoteTeapot,
|
||||
EmoteThinkerState,
|
||||
EmoteWarmHands,
|
||||
EmoteWaveState,
|
||||
EmoteWaveLow,
|
||||
EmoteWaveHigh,
|
||||
EmoteWinded,
|
||||
EmoteWindedState,
|
||||
EmoteWoah,
|
||||
EmoteWoahState,
|
||||
EmoteYawnAndStretch,
|
||||
EmoteYmca,
|
||||
|
||||
SelectionSelf,
|
||||
SelectionPlaceInInventory,
|
||||
SelectionUseClosestUnopenedCorpse,
|
||||
SelectionUseNextUnopenedCorpse,
|
||||
SelectionGiveToTarget,
|
||||
SelectionDrop,
|
||||
SelectionPlaceInMainPack,
|
||||
SelectionClosestUnopenedCorpse,
|
||||
SelectionNextUnopenedCorpse,
|
||||
|
||||
ToggleAbuseReportingPanel,
|
||||
ToggleCharacterInfoPanel,
|
||||
TogglePositiveMagicPanel,
|
||||
ToggleNegativeMagicPanel,
|
||||
ToggleLinkStatusPanel,
|
||||
ToggleUrgentAssistancePanel,
|
||||
ToggleVitaePanel,
|
||||
ToggleSocialPanel,
|
||||
ToggleSpellManagementPanel,
|
||||
ToggleCharacterDetailPanel,
|
||||
ToggleMapPage,
|
||||
ToggleHousePage,
|
||||
ToggleGameplayOptionsPage,
|
||||
ToggleCharacterSettingsPage,
|
||||
ToggleConfigurationPage,
|
||||
ToggleCompass,
|
||||
ToggleKeyboardConfiguration,
|
||||
ToggleFriendsPage,
|
||||
ToggleCharacterTitlesPage,
|
||||
ToggleQuestDetailPanel,
|
||||
ToggleQuestJournalPage,
|
||||
ToggleJournalPageList,
|
||||
ToggleContractsPage,
|
||||
|
||||
ChatMonarchReply,
|
||||
ChatPatronReply,
|
||||
ChatReply,
|
||||
ChatStartCommand,
|
||||
ChatTellToSelected,
|
||||
|
||||
UseQuickSlot_10,
|
||||
UseQuickSlot_11,
|
||||
UseQuickSlot_12,
|
||||
UseQuickSlot_13,
|
||||
|
||||
ToggleCharacterOptionAutoRepeatAttack,
|
||||
ToggleCharacterOptionIgnoreAllegianceRequests,
|
||||
ToggleCharacterOptionIgnoreFellowshipRequests,
|
||||
ToggleCharacterOptionIgnoreTradeRequests,
|
||||
ToggleCharacterOptionPersistentAtDay,
|
||||
ToggleCharacterOptionAllowGive,
|
||||
ToggleCharacterOptionViewCombatTarget,
|
||||
ToggleCharacterOptionShowTooltips,
|
||||
ToggleCharacterOptionUseDeception,
|
||||
ToggleCharacterOptionToggleRun,
|
||||
ToggleCharacterOptionStayInChatMode,
|
||||
ToggleCharacterOptionAdvancedCombatUi,
|
||||
ToggleCharacterOptionAutoTarget,
|
||||
ToggleCharacterOptionVividTargetingIndicator,
|
||||
ToggleCharacterOptionFellowshipShareXp,
|
||||
ToggleCharacterOptionAcceptLootPermits,
|
||||
ToggleCharacterOptionFellowshipShareLoot,
|
||||
ToggleCharacterOptionFellowshipAutoAcceptRequests,
|
||||
ToggleCharacterOptionCoordinatesOnRadar,
|
||||
ToggleCharacterOptionSpellDuration,
|
||||
ToggleCharacterOptionDisableHouseRestrictionEffects,
|
||||
ToggleCharacterOptionDragItemOnPlayerOpensSecureTrade,
|
||||
ToggleCharacterOptionDisplayAllegianceLogonNotifications,
|
||||
ToggleCharacterOptionUseChargeAttack,
|
||||
ToggleCharacterOptionUseCraftSuccessDialog,
|
||||
ToggleCharacterOptionListenToAllegianceChat,
|
||||
ToggleCharacterOptionDisplayDateOfBirth,
|
||||
ToggleCharacterOptionDisplayAge,
|
||||
ToggleCharacterOptionDisplayChessRank,
|
||||
ToggleCharacterOptionDisplayFishingSkill,
|
||||
ToggleCharacterOptionDisplayNumberDeaths,
|
||||
ToggleCharacterOptionDisplayTimeStamps,
|
||||
ToggleCharacterOptionSalvageMultiple,
|
||||
ToggleCharacterOptionListenToGeneralChat,
|
||||
ToggleCharacterOptionListenToTradeChat,
|
||||
ToggleCharacterOptionListenToLfgChat,
|
||||
ToggleCharacterOptionListenToRoleplayChat,
|
||||
ToggleCharacterOptionDisplayNumberCharacterTitles,
|
||||
ToggleCharacterOptionMainPackPreferred,
|
||||
ToggleCharacterOptionLeadMissileTargets,
|
||||
ToggleCharacterOptionUseFastMissiles,
|
||||
ToggleCharacterOptionFilterLanguage,
|
||||
ToggleCharacterOptionConfirmVolatileRareUse,
|
||||
ToggleCharacterOptionListenToSocietyChat,
|
||||
ToggleCharacterOptionShowHelm,
|
||||
ToggleCharacterOptionDisableDistanceFog,
|
||||
ToggleCharacterOptionShowCloak,
|
||||
ToggleCharacterOptionSideBySideVitals,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,9 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// K.1a wiring: <c>GameWindow</c> constructs a dispatcher alongside the
|
||||
/// existing <c>IsKeyPressed</c> + event-handler paths. Nothing
|
||||
/// subscribes to <see cref="Fired"/> yet except a diagnostic console
|
||||
/// logger — the dispatcher is observable but doesn't drive any
|
||||
/// behavior. K.1b cuts the existing handlers over to the dispatcher's
|
||||
/// action stream.
|
||||
/// The production gameplay router is the sole gameplay subscriber; retained
|
||||
/// UI, selection, camera, combat, movement, and commands all receive semantic
|
||||
/// actions through this stream.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class InputDispatcher : IDisposable
|
||||
|
|
@ -38,6 +35,7 @@ public sealed class InputDispatcher : IDisposable
|
|||
private KeyBindings _bindings;
|
||||
private readonly Stack<InputScope> _scopes = new();
|
||||
private InputScope? _combatScope;
|
||||
private bool _cameraAlternateScope;
|
||||
private readonly HashSet<KeyChord> _heldHoldChords = new();
|
||||
private readonly HashSet<InputAction> _automationHeldActions = new();
|
||||
private readonly Dictionary<MouseButton, float> _mouseClickTravel = new();
|
||||
|
|
@ -55,16 +53,28 @@ public sealed class InputDispatcher : IDisposable
|
|||
private const long DoubleClickThresholdMs = 500;
|
||||
private const float ClickDragThresholdPixels = 3f;
|
||||
|
||||
/// <summary>K.3 modal-rebind hook: when non-null, the next non-modifier
|
||||
/// chord is reported via this callback INSTEAD of firing actions. Esc
|
||||
/// cancels (callback receives <c>default(KeyChord)</c>).</summary>
|
||||
/// <summary>K.3 modal-rebind hook: when non-null, the next complete key or
|
||||
/// mouse chord is reported via this callback INSTEAD of firing actions.
|
||||
/// A modifier key is deferred until release so it can be captured alone or
|
||||
/// used as a prefix. Esc cancels (callback receives
|
||||
/// <c>default(KeyChord)</c>).</summary>
|
||||
private Action<KeyChord>? _captureCallback;
|
||||
private Key? _captureModifierCandidate;
|
||||
private KeyChord? _currentPhysicalChord;
|
||||
|
||||
/// <summary>Fires every time a binding matches a press, release, hold,
|
||||
/// complete click, or double-click.
|
||||
/// Multicast — every subscriber gets every event in subscription order.</summary>
|
||||
public event Action<InputAction, ActivationType>? Fired;
|
||||
|
||||
/// <summary>
|
||||
/// The keyboard chord whose native key-down callback is synchronously
|
||||
/// publishing <see cref="Fired"/>, or <see langword="null"/> outside that
|
||||
/// callback. This lets retained UI suppress the raw tail of the same key
|
||||
/// after a semantic action has just moved keyboard focus.
|
||||
/// </summary>
|
||||
public KeyChord? CurrentPhysicalChord => _currentPhysicalChord;
|
||||
|
||||
private InputDispatcher(
|
||||
IKeyboardSource keyboard,
|
||||
IMouseSource mouse,
|
||||
|
|
@ -158,9 +168,11 @@ public sealed class InputDispatcher : IDisposable
|
|||
{
|
||||
Interlocked.Exchange(ref _active, 0);
|
||||
_captureCallback = null;
|
||||
_captureModifierCandidate = null;
|
||||
_heldHoldChords.Clear();
|
||||
_automationHeldActions.Clear();
|
||||
_mouseClickTravel.Clear();
|
||||
_cameraAlternateScope = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -226,9 +238,24 @@ public sealed class InputDispatcher : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>Topmost scope on the stack — what the dispatcher looks up first.</summary>
|
||||
public InputScope ActiveScope => _scopes.Peek() == InputScope.Game && _combatScope is { } combat
|
||||
? combat
|
||||
: _scopes.Peek();
|
||||
public InputScope ActiveScope => _cameraAlternateScope
|
||||
? InputScope.Camera
|
||||
: _scopes.Peek() == InputScope.Game && _combatScope is { } combat
|
||||
? combat
|
||||
: _scopes.Peek();
|
||||
|
||||
/// <summary>
|
||||
/// Installs retail InputMap 6 while the camera-mode chord (F2 or keypad
|
||||
/// divide by default) is physically held. Retail registers this map at
|
||||
/// priority 2000 over the ordinary priority-1000 maps, so its arrow-key
|
||||
/// bindings shadow movement without replacing the normal scope stack.
|
||||
/// </summary>
|
||||
public void SetCameraAlternateScope(bool active)
|
||||
{
|
||||
if (_cameraAlternateScope == active) return;
|
||||
ReleaseHeldHoldBindings();
|
||||
_cameraAlternateScope = active;
|
||||
}
|
||||
|
||||
/// <summary>Set the mode-dependent combat layer that shadows normal game chords.</summary>
|
||||
public void SetCombatScope(InputScope? scope)
|
||||
|
|
@ -243,34 +270,72 @@ public sealed class InputDispatcher : IDisposable
|
|||
|
||||
private Binding? FindActive(KeyChord chord, ActivationType activation)
|
||||
{
|
||||
IReadOnlyList<Binding> bindings = FindActiveBindings(chord, activation);
|
||||
return bindings.Count == 0 ? null : bindings[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns every binding in the highest-priority active retail InputMap.
|
||||
/// Retail's shipped map deliberately assigns Alt+1..4 in both UICommands
|
||||
/// and QuickslotCommands; ICIDM emits both actions because those maps are
|
||||
/// simultaneously active. A first-match lookup made half of those exact
|
||||
/// defaults unreachable.
|
||||
/// </summary>
|
||||
private IReadOnlyList<Binding> FindActiveBindings(
|
||||
KeyChord chord,
|
||||
ActivationType activation)
|
||||
{
|
||||
if (_cameraAlternateScope)
|
||||
{
|
||||
Binding[] camera = FindInScope(InputScope.Camera, chord, activation);
|
||||
if (camera.Length != 0)
|
||||
return camera;
|
||||
}
|
||||
|
||||
foreach (InputScope scope in _scopes)
|
||||
{
|
||||
if (scope == InputScope.Game && _combatScope is { } combat
|
||||
&& _bindings.Find(chord, activation, combat) is { } combatBinding)
|
||||
return combatBinding;
|
||||
if (_bindings.Find(chord, activation, scope) is { } binding)
|
||||
return binding;
|
||||
if (scope == InputScope.Game && _combatScope is { } combat)
|
||||
{
|
||||
Binding[] combatBindings = FindInScope(combat, chord, activation);
|
||||
if (combatBindings.Length != 0)
|
||||
return combatBindings;
|
||||
}
|
||||
|
||||
Binding[] bindings = FindInScope(scope, chord, activation);
|
||||
if (bindings.Length != 0)
|
||||
return bindings;
|
||||
}
|
||||
return null;
|
||||
return Array.Empty<Binding>();
|
||||
}
|
||||
|
||||
private Binding[] FindInScope(
|
||||
InputScope scope,
|
||||
KeyChord chord,
|
||||
ActivationType activation) =>
|
||||
_bindings.All
|
||||
.Where(binding =>
|
||||
binding.Scope == scope
|
||||
&& binding.Chord == chord
|
||||
&& binding.Activation == activation)
|
||||
.DistinctBy(static binding => binding.Action)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>True iff a <see cref="BeginCapture"/> is in progress.</summary>
|
||||
public bool IsCapturing => _captureCallback is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Enter modal capture mode. The next non-modifier chord pressed
|
||||
/// (with whatever modifiers are held at that moment) is reported
|
||||
/// via <paramref name="onCaptured"/> and the dispatcher does NOT
|
||||
/// fire normal action events for that chord. Esc cancels —
|
||||
/// <paramref name="onCaptured"/> receives a sentinel
|
||||
/// <c>default(KeyChord)</c>. Modifier-only key transitions
|
||||
/// (Shift / Ctrl / Alt / Win held alone) are NOT captured; only a
|
||||
/// non-modifier key down completes capture, so the user can dial
|
||||
/// in modifier combinations before pressing the trigger key.
|
||||
/// Enter modal capture mode. The next keyboard key or mouse button
|
||||
/// (with whatever modifiers are held at that moment) is reported via
|
||||
/// <paramref name="onCaptured"/> and the dispatcher does NOT fire normal
|
||||
/// actions for that chord. Shift/Ctrl/Alt/Win are deferred until key-up:
|
||||
/// pressing another key first makes them a prefix; releasing the modifier
|
||||
/// first captures the modifier-only binding. Esc cancels and reports
|
||||
/// <c>default(KeyChord)</c>.
|
||||
/// </summary>
|
||||
public void BeginCapture(Action<KeyChord> onCaptured)
|
||||
{
|
||||
_captureCallback = onCaptured ?? throw new ArgumentNullException(nameof(onCaptured));
|
||||
_captureModifierCandidate = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -283,6 +348,7 @@ public sealed class InputDispatcher : IDisposable
|
|||
var cb = _captureCallback;
|
||||
if (cb is null) return;
|
||||
_captureCallback = null;
|
||||
_captureModifierCandidate = null;
|
||||
cb(default);
|
||||
}
|
||||
|
||||
|
|
@ -440,8 +506,7 @@ public sealed class InputDispatcher : IDisposable
|
|||
if (_heldHoldChords.Count == 0) return;
|
||||
var releases = new List<Binding>(_heldHoldChords.Count);
|
||||
foreach (KeyChord chord in _heldHoldChords)
|
||||
if (FindActive(chord, ActivationType.Hold) is { } binding)
|
||||
releases.Add(binding);
|
||||
releases.AddRange(FindActiveBindings(chord, ActivationType.Hold));
|
||||
_heldHoldChords.Clear();
|
||||
foreach (Binding binding in releases)
|
||||
Fired?.Invoke(binding.Action, ActivationType.Release);
|
||||
|
|
@ -469,9 +534,8 @@ public sealed class InputDispatcher : IDisposable
|
|||
// chord; never dispatch a stale snapshot entry afterward.
|
||||
if (!_heldHoldChords.Contains(chord))
|
||||
continue;
|
||||
var hold = FindActive(chord, ActivationType.Hold);
|
||||
if (hold is not null)
|
||||
Fired?.Invoke(hold.Value.Action, ActivationType.Hold);
|
||||
foreach (Binding hold in FindActiveBindings(chord, ActivationType.Hold))
|
||||
Fired?.Invoke(hold.Action, ActivationType.Hold);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -480,50 +544,62 @@ public sealed class InputDispatcher : IDisposable
|
|||
if (Volatile.Read(ref _active) == 0) return;
|
||||
// K.3 modal capture (used by Settings panel's "Rebind" UX) takes
|
||||
// precedence over both WantCaptureKeyboard gating AND normal
|
||||
// binding lookup. Esc cancels capture; modifier-only keys don't
|
||||
// complete it (so the user can dial in Shift/Ctrl/Alt before
|
||||
// pressing the trigger key); every other key completes capture
|
||||
// with the current modifier state.
|
||||
// binding lookup. Esc cancels capture. A modifier key is deferred
|
||||
// until its key-up so it can either become the primary binding by
|
||||
// itself (retail's walk-mode default) or remain a prefix when the
|
||||
// user presses a non-modifier key before releasing it.
|
||||
if (_captureCallback is not null)
|
||||
{
|
||||
if (key == Key.Escape)
|
||||
{
|
||||
var cb = _captureCallback;
|
||||
_captureCallback = null;
|
||||
_captureModifierCandidate = null;
|
||||
cb(default);
|
||||
return;
|
||||
}
|
||||
if (IsModifierKey(key)) return; // dial more mods, don't complete
|
||||
if (IsModifierKey(key))
|
||||
{
|
||||
_captureModifierCandidate = key;
|
||||
return;
|
||||
}
|
||||
|
||||
var captured = new KeyChord(key, mods, Device: 0);
|
||||
var cb2 = _captureCallback;
|
||||
_captureCallback = null;
|
||||
_captureModifierCandidate = null;
|
||||
cb2(captured);
|
||||
return; // SUPPRESS the action — don't run binding lookup below
|
||||
}
|
||||
|
||||
if (_mouse.WantCaptureKeyboard) return;
|
||||
var chord = new KeyChord(key, mods, Device: 0);
|
||||
|
||||
var press = FindActive(chord, ActivationType.Press);
|
||||
if (press is not null) Fired?.Invoke(press.Value.Action, ActivationType.Press);
|
||||
|
||||
var click = FindActive(chord, ActivationType.Click);
|
||||
if (click is not null) Fired?.Invoke(click.Value.Action, ActivationType.Click);
|
||||
|
||||
var hold = FindActive(chord, ActivationType.Hold);
|
||||
if (hold is not null)
|
||||
var chord = KeyboardChord(key, mods);
|
||||
_currentPhysicalChord = chord;
|
||||
try
|
||||
{
|
||||
// Emit a Press transition so subscribers can latch state, then
|
||||
// record the chord so Tick() will re-fire Hold every frame.
|
||||
Fired?.Invoke(hold.Value.Action, ActivationType.Press);
|
||||
_heldHoldChords.Add(chord);
|
||||
foreach (Binding press in FindActiveBindings(chord, ActivationType.Press))
|
||||
Fired?.Invoke(press.Action, ActivationType.Press);
|
||||
|
||||
foreach (Binding click in FindActiveBindings(chord, ActivationType.Click))
|
||||
Fired?.Invoke(click.Action, ActivationType.Click);
|
||||
|
||||
IReadOnlyList<Binding> holds = FindActiveBindings(chord, ActivationType.Hold);
|
||||
if (holds.Count != 0)
|
||||
{
|
||||
// Emit a Press transition so subscribers can latch state, then
|
||||
// record the chord so Tick() will re-fire Hold every frame.
|
||||
foreach (Binding hold in holds)
|
||||
Fired?.Invoke(hold.Action, ActivationType.Press);
|
||||
_heldHoldChords.Add(chord);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentPhysicalChord = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>True for Shift/Ctrl/Alt/Win left+right variants — keys
|
||||
/// that don't complete a capture by themselves. The user holds them
|
||||
/// to dial in modifier combinations before pressing the trigger key.</summary>
|
||||
/// <summary>True for Shift/Ctrl/Alt/Win left+right variants.</summary>
|
||||
private static bool IsModifierKey(Key key) => key switch
|
||||
{
|
||||
Key.ShiftLeft or Key.ShiftRight => true,
|
||||
|
|
@ -536,13 +612,24 @@ public sealed class InputDispatcher : IDisposable
|
|||
private void OnKeyUp(Key key, ModifierMask mods)
|
||||
{
|
||||
if (Volatile.Read(ref _active) == 0) return;
|
||||
if (_captureCallback is not null)
|
||||
{
|
||||
if (_captureModifierCandidate == key)
|
||||
{
|
||||
Action<KeyChord> callback = _captureCallback;
|
||||
_captureCallback = null;
|
||||
_captureModifierCandidate = null;
|
||||
callback(KeyboardChord(key, mods));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Release fires regardless of WantCaptureKeyboard so we don't
|
||||
// strand a Hold subscriber in the "held" state if the UI captured
|
||||
// mid-press.
|
||||
var chord = new KeyChord(key, mods, Device: 0);
|
||||
var chord = KeyboardChord(key, mods);
|
||||
|
||||
var release = FindActive(chord, ActivationType.Release);
|
||||
if (release is not null) Fired?.Invoke(release.Value.Action, ActivationType.Release);
|
||||
foreach (Binding release in FindActiveBindings(chord, ActivationType.Release))
|
||||
Fired?.Invoke(release.Action, ActivationType.Release);
|
||||
|
||||
// Any matching Hold binding gets a Release transition. Walk the
|
||||
// tracked set looking for a chord with a matching Key (ignoring
|
||||
|
|
@ -556,8 +643,8 @@ public sealed class InputDispatcher : IDisposable
|
|||
foreach (var held in toRemove)
|
||||
{
|
||||
_heldHoldChords.Remove(held);
|
||||
var hold = FindActive(held, ActivationType.Hold);
|
||||
if (hold is not null) Fired?.Invoke(hold.Value.Action, ActivationType.Release);
|
||||
foreach (Binding hold in FindActiveBindings(held, ActivationType.Hold))
|
||||
Fired?.Invoke(hold.Action, ActivationType.Release);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -565,16 +652,33 @@ public sealed class InputDispatcher : IDisposable
|
|||
{
|
||||
if (Volatile.Read(ref _active) == 0) return;
|
||||
_mouseClickTravel.Remove(button);
|
||||
// Retail UIOption_ActionKeyMap captures a QualifiedControl, not merely
|
||||
// a keyboard scan code. Mouse buttons therefore use the same modal
|
||||
// capture path and suppress their ordinary action, even while the UI
|
||||
// owns the pointer for the binding dialog.
|
||||
if (_captureCallback is not null)
|
||||
{
|
||||
var captured = new KeyChord(
|
||||
MouseButtonToKey(button),
|
||||
mods,
|
||||
Device: 1);
|
||||
Action<KeyChord> callback = _captureCallback;
|
||||
_captureCallback = null;
|
||||
_captureModifierCandidate = null;
|
||||
callback(captured);
|
||||
return;
|
||||
}
|
||||
if (_mouse.WantCaptureMouse) return;
|
||||
var chord = new KeyChord(MouseButtonToKey(button), mods, Device: 1);
|
||||
|
||||
var press = FindActive(chord, ActivationType.Press);
|
||||
if (press is not null) Fired?.Invoke(press.Value.Action, ActivationType.Press);
|
||||
foreach (Binding press in FindActiveBindings(chord, ActivationType.Press))
|
||||
Fired?.Invoke(press.Action, ActivationType.Press);
|
||||
|
||||
var hold = FindActive(chord, ActivationType.Hold);
|
||||
if (hold is not null)
|
||||
IReadOnlyList<Binding> holds = FindActiveBindings(chord, ActivationType.Hold);
|
||||
if (holds.Count != 0)
|
||||
{
|
||||
Fired?.Invoke(hold.Value.Action, ActivationType.Press);
|
||||
foreach (Binding hold in holds)
|
||||
Fired?.Invoke(hold.Action, ActivationType.Press);
|
||||
_heldHoldChords.Add(chord);
|
||||
}
|
||||
|
||||
|
|
@ -589,8 +693,8 @@ public sealed class InputDispatcher : IDisposable
|
|||
if (_lastMouseDownButton == button
|
||||
&& nowMs - _lastMouseDownTickMs <= DoubleClickThresholdMs)
|
||||
{
|
||||
var dbl = FindActive(chord, ActivationType.DoubleClick);
|
||||
if (dbl is not null) Fired?.Invoke(dbl.Value.Action, ActivationType.DoubleClick);
|
||||
foreach (Binding dbl in FindActiveBindings(chord, ActivationType.DoubleClick))
|
||||
Fired?.Invoke(dbl.Action, ActivationType.DoubleClick);
|
||||
_lastMouseDownButton = null; // consumed; require fresh pair for next
|
||||
}
|
||||
else
|
||||
|
|
@ -606,8 +710,8 @@ public sealed class InputDispatcher : IDisposable
|
|||
var chord = new KeyChord(MouseButtonToKey(button), mods, Device: 1);
|
||||
bool wasClickCandidate = _mouseClickTravel.Remove(button, out float travel);
|
||||
|
||||
var release = FindActive(chord, ActivationType.Release);
|
||||
if (release is not null) Fired?.Invoke(release.Value.Action, ActivationType.Release);
|
||||
foreach (Binding release in FindActiveBindings(chord, ActivationType.Release))
|
||||
Fired?.Invoke(release.Action, ActivationType.Release);
|
||||
|
||||
var keyForLookup = MouseButtonToKey(button);
|
||||
var toRemove = new List<KeyChord>();
|
||||
|
|
@ -619,16 +723,17 @@ public sealed class InputDispatcher : IDisposable
|
|||
foreach (var held in toRemove)
|
||||
{
|
||||
_heldHoldChords.Remove(held);
|
||||
var hold = FindActive(held, ActivationType.Hold);
|
||||
if (hold is not null) Fired?.Invoke(hold.Value.Action, ActivationType.Release);
|
||||
foreach (Binding hold in FindActiveBindings(held, ActivationType.Hold))
|
||||
Fired?.Invoke(hold.Action, ActivationType.Release);
|
||||
}
|
||||
|
||||
if (wasClickCandidate
|
||||
&& !_mouse.WantCaptureMouse
|
||||
&& travel <= ClickDragThresholdPixels
|
||||
&& FindActive(chord, ActivationType.Click) is { } click)
|
||||
&& FindActiveBindings(chord, ActivationType.Click) is { Count: > 0 } clicks)
|
||||
{
|
||||
Fired?.Invoke(click.Action, ActivationType.Click);
|
||||
foreach (Binding click in clicks)
|
||||
Fired?.Invoke(click.Action, ActivationType.Click);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -683,6 +788,25 @@ public sealed class InputDispatcher : IDisposable
|
|||
_ => (Key)(-1000 - (int)button),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Silk includes a modifier key's own bit in the event modifier mask;
|
||||
/// retail QualifiedControl stores a bare DIK_LSHIFT/LCONTROL/LMENU with
|
||||
/// metamode zero. Remove only the primary key's self bit so persisted and
|
||||
/// displayed chords remain byte-faithful while combinations stay exact.
|
||||
/// </summary>
|
||||
private static KeyChord KeyboardChord(Key key, ModifierMask modifiers)
|
||||
{
|
||||
modifiers &= key switch
|
||||
{
|
||||
Key.ShiftLeft or Key.ShiftRight => ~ModifierMask.Shift,
|
||||
Key.ControlLeft or Key.ControlRight => ~ModifierMask.Ctrl,
|
||||
Key.AltLeft or Key.AltRight => ~ModifierMask.Alt,
|
||||
Key.SuperLeft or Key.SuperRight => ~ModifierMask.Win,
|
||||
_ => ~ModifierMask.None,
|
||||
};
|
||||
return new KeyChord(key, modifiers, Device: 0);
|
||||
}
|
||||
|
||||
private List<Exception> DetachSources()
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
|
|
|
|||
|
|
@ -8,11 +8,8 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// sits at the bottom of the stack and catches global chords like
|
||||
/// Esc / F1 that should fire regardless of focus.
|
||||
///
|
||||
/// <para>
|
||||
/// K.1a defines the enum but only pushes <see cref="Always"/> +
|
||||
/// <see cref="Game"/> by default. Combat scopes light up in Phase L
|
||||
/// when <c>CombatState.CurrentMode</c> tracking lands.
|
||||
/// </para>
|
||||
/// <para>Combat scope follows the live retail combat mode; modal/edit/chat
|
||||
/// scopes are pushed above it as their authored surfaces activate.</para>
|
||||
/// </summary>
|
||||
public enum InputScope
|
||||
{
|
||||
|
|
@ -30,13 +27,13 @@ public enum InputScope
|
|||
/// <summary>A modal dialog is open and capturing input.</summary>
|
||||
Dialog,
|
||||
/// <summary>Combat with melee weapon equipped — Insert/PgUp/Delete/End/PgDn
|
||||
/// remap to power + attack-level. Dormant until Phase L.</summary>
|
||||
/// remap to power + attack-level.</summary>
|
||||
MeleeCombat,
|
||||
/// <summary>Combat with missile weapon equipped — Insert/PgUp/Delete/End/PgDn
|
||||
/// remap to accuracy + aim-level. Dormant until Phase L.</summary>
|
||||
/// remap to accuracy + aim-level.</summary>
|
||||
MissileCombat,
|
||||
/// <summary>Magic mode — 1-9 cast <c>UseSpellSlot</c>; Insert/PgUp etc.
|
||||
/// page through spell tabs. Dormant until Phase L.</summary>
|
||||
/// page through spell tabs.</summary>
|
||||
MagicCombat,
|
||||
/// <summary>Camera alternate mode (F2 / Numpad-/) — arrow keys rotate
|
||||
/// the camera instead of the character.</summary>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// <summary>
|
||||
/// Mutable collection of <see cref="Binding"/>s. Owns lookup by chord
|
||||
/// (for the dispatcher) and lookup by action (for the Settings UI).
|
||||
/// Insertion-order preserved — first-match-wins on lookup, so a user
|
||||
/// can add a custom binding ahead of a default and have it take effect
|
||||
/// without removing the default.
|
||||
/// Insertion order is preserved. Direct <see cref="Find(KeyChord, ActivationType)"/>
|
||||
/// queries return the first match; <see cref="InputDispatcher"/> emits every
|
||||
/// distinct action in the highest-priority active retail input map.
|
||||
///
|
||||
/// <para>
|
||||
/// K.1c: <see cref="RetailDefaults"/> now returns the full retail-faithful
|
||||
|
|
@ -26,7 +26,7 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// </summary>
|
||||
public sealed class KeyBindings
|
||||
{
|
||||
private const int CurrentSchemaVersion = 5;
|
||||
private const int CurrentSchemaVersion = 7;
|
||||
|
||||
private readonly List<Binding> _bindings = new();
|
||||
|
||||
|
|
@ -162,15 +162,9 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.C, ModifierMask.None), InputAction.MovementStrafeRight));
|
||||
b.Add(new(new KeyChord(Key.D, ModifierMask.Alt), InputAction.MovementStrafeRight));
|
||||
b.Add(new(new KeyChord(Key.Right, ModifierMask.Alt), InputAction.MovementStrafeRight));
|
||||
// Walk-mode modifier — Hold so a subscriber can latch state on
|
||||
// press and unlatch on release. K-fix1 (2026-04-26): the chord
|
||||
// modifier MUST be Shift, not None — when LShift/RShift is the
|
||||
// primary key the OS keyboard reports CurrentModifiers=Shift
|
||||
// alongside the key-down. Bind both left + right shift to match.
|
||||
// This is the same pattern AcdreamCurrentDefaults uses for its
|
||||
// Shift→RunLock binding (see lines 98-99 above).
|
||||
b.Add(new(new KeyChord(Key.ShiftLeft, ModifierMask.Shift), InputAction.MovementWalkMode, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.ShiftRight, ModifierMask.Shift), InputAction.MovementWalkMode, ActivationType.Hold));
|
||||
// Retail authors exactly bare DIK_LSHIFT. InputDispatcher normalizes
|
||||
// Silk's self-reported Shift modifier bit at the physical boundary.
|
||||
b.Add(new(new KeyChord(Key.ShiftLeft, ModifierMask.None), InputAction.MovementWalkMode, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.Q, ModifierMask.None), InputAction.MovementRunLock));
|
||||
b.Add(new(new KeyChord(Key.S, ModifierMask.None), InputAction.MovementStop));
|
||||
b.Add(new(new KeyChord(Key.Y, ModifierMask.None), InputAction.Ready));
|
||||
|
|
@ -180,7 +174,10 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.Space, ModifierMask.None), InputAction.MovementJump));
|
||||
|
||||
// ── ItemSelectionCommands ──────────────────────────────
|
||||
b.Add(new(new KeyChord(Key.F, ModifierMask.None), InputAction.SelectionPickUp));
|
||||
// Retail action 0x1000002C: F places the selected object in the
|
||||
// inventory. The old SelectionPickUp alias had no ActionMap row and
|
||||
// made Configure Keyboard show the F default on the wrong command.
|
||||
b.Add(new(new KeyChord(Key.F, ModifierMask.None), InputAction.SelectionPlaceInInventory));
|
||||
b.Add(new(new KeyChord(Key.T, ModifierMask.None), InputAction.SelectionSplitStack));
|
||||
b.Add(new(new KeyChord(Key.P, ModifierMask.None), InputAction.SelectionPreviousSelection));
|
||||
b.Add(new(new KeyChord(Key.Backspace, ModifierMask.None), InputAction.SelectionClosestCompassItem));
|
||||
|
|
@ -223,20 +220,21 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.Escape, ModifierMask.Shift), InputAction.LOGOUT));
|
||||
|
||||
// ── QuickslotCommands ──────────────────────────────────
|
||||
// Retail gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0 receives
|
||||
// distinct action-id ranges: bare 1..9 USE slots 0..8, while
|
||||
// Ctrl+1..9 SELECT those slots. The keymap repeats the display name
|
||||
// UseQuickSlot_N for both bindings, so our semantic action layer must
|
||||
// preserve the differing intent explicitly.
|
||||
// Retail's MasterInputMap binds both bare N and Ctrl+N to the SAME
|
||||
// UseQuickSlot_N action ids (0x10000042..4A). The separate Select
|
||||
// Quickslot action ids (0x1000004E..56) have no default chords.
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
var k = (Key)((int)Key.Number0 + i); // Number1..Number9
|
||||
var useAction = (InputAction)((int)InputAction.UseQuickSlot_1 + i - 1);
|
||||
var selectAction = (InputAction)((int)InputAction.SelectQuickSlot_1 + i - 1);
|
||||
b.Add(new(new KeyChord(k, ModifierMask.None), useAction));
|
||||
b.Add(new(new KeyChord(k, ModifierMask.Ctrl), selectAction));
|
||||
b.Add(new(new KeyChord(k, ModifierMask.Ctrl), useAction));
|
||||
}
|
||||
// Alt+5..9 → UseQuickSlot_14..18.
|
||||
// Alt+1..4 → slots 10..13; Alt+5..9 → slots 14..18.
|
||||
b.Add(new(new KeyChord(Key.Number1, ModifierMask.Alt), InputAction.UseQuickSlot_10));
|
||||
b.Add(new(new KeyChord(Key.Number2, ModifierMask.Alt), InputAction.UseQuickSlot_11));
|
||||
b.Add(new(new KeyChord(Key.Number3, ModifierMask.Alt), InputAction.UseQuickSlot_12));
|
||||
b.Add(new(new KeyChord(Key.Number4, ModifierMask.Alt), InputAction.UseQuickSlot_13));
|
||||
for (int i = 5; i <= 9; i++)
|
||||
{
|
||||
var k = (Key)((int)Key.Number0 + i);
|
||||
|
|
@ -250,7 +248,7 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.Tab, ModifierMask.None), InputAction.ToggleChatEntry));
|
||||
b.Add(new(new KeyChord(Key.Enter, ModifierMask.None), InputAction.EnterChatMode));
|
||||
|
||||
// ── Combat (mode-dependent — dormant in K, lights up in Phase L) ──
|
||||
// ── Combat (mode-dependent retail scopes) ──
|
||||
b.Add(new(new KeyChord(Key.GraveAccent, ModifierMask.None), InputAction.CombatToggleCombat));
|
||||
// Melee mode (active when MeleeCombat scope pushed).
|
||||
b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseAttackPower, Scope: InputScope.MeleeCombat));
|
||||
|
|
@ -261,15 +259,13 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatLowAttack, ActivationType.Hold, InputScope.MeleeCombat));
|
||||
b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatMediumAttack, ActivationType.Hold, InputScope.MeleeCombat));
|
||||
b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatHighAttack, ActivationType.Hold, InputScope.MeleeCombat));
|
||||
// Missile + Magic + Spell-tab — same chords; resolved by scope at
|
||||
// runtime per InputDispatcher's stack lookup. Add the bindings;
|
||||
// subscribers arrive in Phase L when CombatState.CurrentMode is
|
||||
// wired.
|
||||
// Missile + Magic + Spell-tab — same chords; resolved by the live
|
||||
// combat scope through InputDispatcher's stack lookup.
|
||||
b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseMissileAccuracy, Scope: InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatIncreaseMissileAccuracy, Scope: InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatAimLow, Scope: InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatAimMedium, Scope: InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatAimHigh, Scope: InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatAimLow, ActivationType.Hold, InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatAimMedium, ActivationType.Hold, InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatAimHigh, ActivationType.Hold, InputScope.MissileCombat));
|
||||
b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatPrevSpellTab, Scope: InputScope.MagicCombat));
|
||||
b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatNextSpellTab, Scope: InputScope.MagicCombat));
|
||||
b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatPrevSpell, Scope: InputScope.MagicCombat));
|
||||
|
|
@ -294,8 +290,8 @@ public sealed class KeyBindings
|
|||
b.Add(new(new KeyChord(Key.K, ModifierMask.None), InputAction.PointState));
|
||||
|
||||
// ── Camera ─────────────────────────────────────────────
|
||||
b.Add(new(new KeyChord(Key.KeypadDivide, ModifierMask.None), InputAction.CameraActivateAlternateMode));
|
||||
b.Add(new(new KeyChord(Key.F2, ModifierMask.None), InputAction.CameraActivateAlternateMode));
|
||||
b.Add(new(new KeyChord(Key.KeypadDivide, ModifierMask.None), InputAction.CameraActivateAlternateMode, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.F2, ModifierMask.None), InputAction.CameraActivateAlternateMode, ActivationType.Hold));
|
||||
// CameraInstantMouseLook (MMB hold) — encoded as a mouse chord
|
||||
// via the K.1a Device=1 convention. K.2 lights up the actual
|
||||
// camera+yaw drive logic.
|
||||
|
|
@ -304,16 +300,22 @@ public sealed class KeyBindings
|
|||
InputAction.CameraInstantMouseLook,
|
||||
ActivationType.Hold));
|
||||
// Numpad cluster.
|
||||
b.Add(new(new KeyChord(Key.Keypad4, ModifierMask.None), InputAction.CameraRotateLeft));
|
||||
b.Add(new(new KeyChord(Key.Keypad6, ModifierMask.None), InputAction.CameraRotateRight));
|
||||
b.Add(new(new KeyChord(Key.Keypad8, ModifierMask.None), InputAction.CameraRotateUp));
|
||||
b.Add(new(new KeyChord(Key.Keypad2, ModifierMask.None), InputAction.CameraRotateDown));
|
||||
b.Add(new(new KeyChord(Key.KeypadSubtract, ModifierMask.None), InputAction.CameraMoveToward));
|
||||
b.Add(new(new KeyChord(Key.KeypadAdd, ModifierMask.None), InputAction.CameraMoveAway));
|
||||
b.Add(new(new KeyChord(Key.Keypad4, ModifierMask.None), InputAction.CameraRotateLeft, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.Keypad6, ModifierMask.None), InputAction.CameraRotateRight, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.Keypad8, ModifierMask.None), InputAction.CameraRotateUp, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.Keypad2, ModifierMask.None), InputAction.CameraRotateDown, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.KeypadSubtract, ModifierMask.None), InputAction.CameraMoveToward, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.KeypadAdd, ModifierMask.None), InputAction.CameraMoveAway, ActivationType.Hold));
|
||||
b.Add(new(new KeyChord(Key.Keypad0, ModifierMask.None), InputAction.CameraViewDefault));
|
||||
b.Add(new(new KeyChord(Key.KeypadDecimal, ModifierMask.None), InputAction.CameraViewFirstPerson));
|
||||
b.Add(new(new KeyChord(Key.Keypad5, ModifierMask.None), InputAction.CameraViewLookDown));
|
||||
b.Add(new(new KeyChord(Key.KeypadEnter, ModifierMask.None), InputAction.CameraViewMapMode));
|
||||
// CameraAlternateControls is a separate retail InputMap. Its arrow
|
||||
// defaults must not alias the movement/camera-primary row identities.
|
||||
b.Add(new(new KeyChord(Key.Left, ModifierMask.None), InputAction.CameraAlternateRotateLeft, ActivationType.Hold, InputScope.Camera));
|
||||
b.Add(new(new KeyChord(Key.Right, ModifierMask.None), InputAction.CameraAlternateRotateRight, ActivationType.Hold, InputScope.Camera));
|
||||
b.Add(new(new KeyChord(Key.Up, ModifierMask.None), InputAction.CameraAlternateRotateUp, ActivationType.Hold, InputScope.Camera));
|
||||
b.Add(new(new KeyChord(Key.Down, ModifierMask.None), InputAction.CameraAlternateRotateDown, ActivationType.Hold, InputScope.Camera));
|
||||
|
||||
// ── Mouse selection ────────────────────────────────────
|
||||
// Retail keymap: SelectLeft = LMB, SelectRight = RMB, SelectMid = MMB,
|
||||
|
|
@ -413,6 +415,7 @@ public sealed class KeyBindings
|
|||
|
||||
var defaults = RetailDefaults();
|
||||
var loaded = new KeyBindings();
|
||||
var explicitlyStoredActions = new HashSet<InputAction>();
|
||||
|
||||
if (root.TryGetProperty("actions", out var actionsEl)
|
||||
&& actionsEl.ValueKind == JsonValueKind.Object)
|
||||
|
|
@ -422,6 +425,7 @@ public sealed class KeyBindings
|
|||
if (!Enum.TryParse<InputAction>(actionProp.Name, out var action))
|
||||
continue; // unknown action → skip
|
||||
if (actionProp.Value.ValueKind != JsonValueKind.Array) continue;
|
||||
explicitlyStoredActions.Add(action);
|
||||
foreach (var bindingEl in actionProp.Value.EnumerateArray())
|
||||
{
|
||||
if (!bindingEl.TryGetProperty("key", out var keyEl)) continue;
|
||||
|
|
@ -444,7 +448,11 @@ public sealed class KeyBindings
|
|||
device = (byte)dEl.GetInt32();
|
||||
}
|
||||
var chord = new KeyChord(silkKey, mods, device);
|
||||
action = MigrateLegacyQuickSlotIntent(version, action, chord, activation);
|
||||
action = MigrateQuickSlotIntent(version, action, chord, activation);
|
||||
// A migrated action is still an explicit user entry.
|
||||
// Without this, the default-merge pass below appends the
|
||||
// retail default beside the migrated custom chord.
|
||||
explicitlyStoredActions.Add(action);
|
||||
activation = MigrateCombatAttackActivation(version, action, activation);
|
||||
activation = MigrateSelectRightActivation(version, action, activation);
|
||||
InputScope scope = defaults.ForAction(action)
|
||||
|
|
@ -468,7 +476,7 @@ public sealed class KeyBindings
|
|||
// newly-added actions if the user file is older.
|
||||
foreach (var actionInDefaults in Enum.GetValues<InputAction>())
|
||||
{
|
||||
if (!loaded.ForAction(actionInDefaults).Any()
|
||||
if (!explicitlyStoredActions.Contains(actionInDefaults)
|
||||
&& defaults.ForAction(actionInDefaults).Any())
|
||||
{
|
||||
foreach (var def in defaults.ForAction(actionInDefaults))
|
||||
|
|
@ -496,6 +504,12 @@ public sealed class KeyBindings
|
|||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
var actions = new SortedDictionary<string, List<object>>(StringComparer.Ordinal);
|
||||
// An empty array is meaningful: the player explicitly cleared all
|
||||
// three GUI slots for this retail action. Earlier schemas omitted the
|
||||
// property entirely, so the next launch mistook "unbound" for "new
|
||||
// action missing from an old file" and silently restored its default.
|
||||
foreach (InputAction action in RetailActionIdentityTable.Map.Values)
|
||||
actions.TryAdd(action.ToString(), new List<object>());
|
||||
foreach (var binding in _bindings)
|
||||
{
|
||||
if (!actions.TryGetValue(binding.Action.ToString(), out var list))
|
||||
|
|
@ -528,30 +542,31 @@ public sealed class KeyBindings
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schema v1 repeated <c>UseQuickSlot_N</c> for bare and Ctrl chords,
|
||||
/// losing retail's use-vs-select distinction. Migrate only the exact old
|
||||
/// default Ctrl+matching-number shape; arbitrary user rebindings remain
|
||||
/// attached to the action the user chose.
|
||||
/// Schema v2-v5 incorrectly rewrote retail's Ctrl+1..9
|
||||
/// <c>UseQuickSlot_N</c> defaults to <c>SelectQuickSlot_N</c>. The 2013
|
||||
/// MasterInputMap and <c>gmToolbarUI::ListenToGlobalMessage</c> both prove
|
||||
/// Ctrl+N sends the same use action as bare N. Repair only that exact old
|
||||
/// generated-default shape; arbitrary SelectQuickSlot rebindings remain.
|
||||
/// </summary>
|
||||
private static InputAction MigrateLegacyQuickSlotIntent(
|
||||
private static InputAction MigrateQuickSlotIntent(
|
||||
int version,
|
||||
InputAction action,
|
||||
KeyChord chord,
|
||||
ActivationType activation)
|
||||
{
|
||||
if (version >= 2
|
||||
if (version >= 6
|
||||
|| activation != ActivationType.Press
|
||||
|| chord.Device != 0
|
||||
|| chord.Modifiers != ModifierMask.Ctrl)
|
||||
return action;
|
||||
|
||||
int offset = (int)action - (int)InputAction.UseQuickSlot_1;
|
||||
int offset = (int)action - (int)InputAction.SelectQuickSlot_1;
|
||||
if ((uint)offset >= 9u)
|
||||
return action;
|
||||
|
||||
var expectedKey = (Key)((int)Key.Number1 + offset);
|
||||
return chord.Key == expectedKey
|
||||
? (InputAction)((int)InputAction.SelectQuickSlot_1 + offset)
|
||||
? (InputAction)((int)InputAction.UseQuickSlot_1 + offset)
|
||||
: action;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,72 +1,138 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AcDream.UI.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP8: maps a retail DAT ActionMap row — the
|
||||
/// <c>(InputMap id, Action id)</c> pair <c>AcDream.Core.Input.RetailActionMapRow</c>
|
||||
/// carries — to acdream's own <see cref="InputAction"/>, when one exists.
|
||||
/// carries — to acdream's own <see cref="InputAction"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Why this table exists.</b> The DAT ActionMap singleton (empirically dumped
|
||||
/// 2026-08-11, see <c>AcDream.Core.Input.RetailActionMap</c>'s class doc) carries 306
|
||||
/// user-bindable rows. <see cref="InputAction"/> — the enum every OTHER acdream input
|
||||
/// path (live dispatch, <c>KeyBindings</c>, <c>InputDispatcher</c>) already keys on —
|
||||
/// has roughly half that many members, because it was authored around "what acdream
|
||||
/// currently implements" (K.1a/K.1c), not "every action the 2013 client's keymap
|
||||
/// screen can show." Two categories are the biggest gaps: 82 of the DAT's 87 Emote
|
||||
/// rows have no acdream animation dispatch yet (only 5 are wired: Cry/Laugh/Cheer/
|
||||
/// Wave/PointState — exactly the 5 that happen to carry retail default keys), and all
|
||||
/// 48 CharacterSettings rows are hotkeys for the SAME <c>PlayerOption</c>/
|
||||
/// <c>CharacterOptions</c> preference bits OP1's <c>CharacterOptionTable</c> and OP4's
|
||||
/// Character-tab checkboxes already model — wiring "press this key, flip that same
|
||||
/// server-synced bit" is a real feature (a hotkey-to-option-toggle dispatcher) that
|
||||
/// does not exist yet anywhere in acdream and is out of scope for this slice (see the
|
||||
/// OP8 register row).
|
||||
/// The DAT ActionMap singleton carries exactly 306 user-bindable rows. Campaign KB
|
||||
/// gives every row one distinct live identity. Identity is the full
|
||||
/// <c>(InputMapId, ActionId)</c> pair: retail legitimately reuses action ids between
|
||||
/// CameraControls and CameraAlternateControls, and collapsing those rows would make
|
||||
/// one GUI rebind silently overwrite the other.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Every mapping below was verified two ways</b> before being added: (1) the DAT's
|
||||
/// <b>Every mapping below was verified two ways</b>: (1) the DAT's
|
||||
/// resolved English label/tooltip unambiguously names the SAME action as the
|
||||
/// <see cref="InputAction"/> member's own XML doc, AND (2) where the retail default
|
||||
/// key(s) for that DAT row are non-empty, they match
|
||||
/// <see cref="KeyBindings.RetailDefaults"/>'s existing chord(s) for the candidate
|
||||
/// <see cref="InputAction"/> (byte-verified 2026-08-11 against the installed dats —
|
||||
/// see <c>RetailActionMapReaderTests.LiveDatTests</c> and this slice's
|
||||
/// <c>RetailActionIdentityRoundTripTests</c>). A DAT row that could not be verified
|
||||
/// BOTH ways is left OUT of this table on purpose — it renders on the Configure
|
||||
/// Keyboard screen as a real, bindable, persisted row (see
|
||||
/// <c>KeyboardConfigController</c>), it just does not yet reach any live acdream
|
||||
/// consumer. Silently guessing a wrong mapping would misroute a user's rebind to the
|
||||
/// WRONG gameplay action, which is worse than an honest "not wired yet."
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Known gaps deliberately left unmapped</b> (register row, OP8):
|
||||
/// Spell Slot 10/11/12 (ctx <c>0x10000005</c>, DAT actions <c>0x6E/0x6F/0x70</c> —
|
||||
/// <see cref="InputAction"/> only defines <c>UseSpellSlot_1..9</c>); Quickslot
|
||||
/// 10/11/12/13 (ctx <c>0x1000000C</c>, DAT actions <c>0x1000004B/4C/4D/10000132</c> —
|
||||
/// <see cref="InputAction"/>'s <c>UseQuickSlot_*</c> family jumps from 9 straight to
|
||||
/// 14, a pre-existing enum gap this slice did not introduce and does not fix); every
|
||||
/// CharacterSettings row (ctx <c>0x10000008</c>, all 48); 82 of 87 Emote rows (ctx
|
||||
/// <c>0x10000006</c>); all 10 CameraAlternateControls rows (ctx <c>0x6</c> — the M2
|
||||
/// de-alias carve-out, see the mapping table's own comment); and roughly half of the
|
||||
/// UI-class rows (ctx <c>0x10000007</c>/<c>0x10000009</c> — panels acdream has no
|
||||
/// toggle for, e.g. Vitae, Link Status, House, Map, Character Info, the
|
||||
/// positive/negative Magic panels).
|
||||
/// <see cref="InputAction"/>. The installed-DAT conformance test requires complete,
|
||||
/// injective 306/306 coverage, so a future DAT drift cannot quietly recreate the
|
||||
/// former dim/store-only tier.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailActionIdentityTable
|
||||
{
|
||||
/// <summary>(InputMap id, Action id) → the acdream <see cref="InputAction"/> that
|
||||
/// owns live dispatch for it. A DAT row whose key is absent has no acdream
|
||||
/// consumer yet.</summary>
|
||||
/// owns live dispatch for it.</summary>
|
||||
public static readonly IReadOnlyDictionary<(uint InputMapId, uint ActionId), InputAction> Map =
|
||||
BuildTable();
|
||||
|
||||
public static bool TryResolve(uint inputMapId, uint actionId, out InputAction action) =>
|
||||
Map.TryGetValue((inputMapId, actionId), out action);
|
||||
|
||||
/// <summary>Inverse identity used by family routers and conformance checks.</summary>
|
||||
public static readonly IReadOnlyDictionary<InputAction, (uint InputMapId, uint ActionId)> ReverseMap =
|
||||
Map.ToDictionary(static pair => pair.Value, static pair => pair.Key);
|
||||
|
||||
public static bool TryGetRetailIdentity(
|
||||
InputAction action,
|
||||
out (uint InputMapId, uint ActionId) identity) =>
|
||||
ReverseMap.TryGetValue(action, out identity);
|
||||
|
||||
/// <summary>Live dispatch scope implied by the retail InputMap context.</summary>
|
||||
public static InputScope ScopeForInputMap(uint inputMapId) => inputMapId switch
|
||||
{
|
||||
0x00000006u => InputScope.Camera,
|
||||
0x10000003u => InputScope.MeleeCombat,
|
||||
0x10000004u => InputScope.MissileCombat,
|
||||
0x10000005u => InputScope.MagicCombat,
|
||||
_ => InputScope.Game,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Retail action delivery shape for a DAT ActionMap row. Continuous
|
||||
/// movement/camera actions receive start and stop edges; the remaining
|
||||
/// rows are one-shot presses unless a concrete retail consumer declares
|
||||
/// otherwise in <see cref="KeyBindings.RetailDefaults"/>.
|
||||
/// </summary>
|
||||
public static ActivationType ActivationFor(uint inputMapId, uint actionId)
|
||||
{
|
||||
if (inputMapId == 0x4u && actionId == 0x32u)
|
||||
return ActivationType.Hold;
|
||||
|
||||
if (inputMapId is 0x5u or 0x6u
|
||||
&& actionId is >= 0x33u and <= 0x38u)
|
||||
{
|
||||
return ActivationType.Hold;
|
||||
}
|
||||
|
||||
if (inputMapId == 0x5u && actionId is 0x3Du or 0x3Eu)
|
||||
return ActivationType.Hold;
|
||||
|
||||
// ClientCombatSystem::HandleCombatAction @ 0x0056D600 sends both
|
||||
// melee 0x5D-0x5F and missile 0xF1-0xF3 through Begin/EndAttackRequest.
|
||||
if (inputMapId == 0x10000003u
|
||||
&& actionId is >= 0x1000005Du and <= 0x1000005Fu)
|
||||
{
|
||||
return ActivationType.Hold;
|
||||
}
|
||||
|
||||
if (inputMapId == 0x10000004u
|
||||
&& actionId is >= 0x100000F1u and <= 0x100000F3u)
|
||||
{
|
||||
return ActivationType.Hold;
|
||||
}
|
||||
|
||||
return ActivationType.Press;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a CharacterSettings hotkey identity to retail's linear
|
||||
/// PlayerOption id. The five 2013 options absent from ActionMap remain
|
||||
/// configurable through the Character page, but correctly have no row
|
||||
/// here.
|
||||
/// </summary>
|
||||
public static bool TryGetCharacterOptionId(InputAction action, out uint optionId)
|
||||
{
|
||||
optionId = 0u;
|
||||
if (!TryGetRetailIdentity(action, out var identity)
|
||||
|| identity.InputMapId != 0x10000008u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
optionId = identity.ActionId switch
|
||||
{
|
||||
>= 0x10000071u and <= 0x10000074u => identity.ActionId - 0x10000071u,
|
||||
>= 0x10000076u and <= 0x10000083u => identity.ActionId - 0x10000071u,
|
||||
>= 0x10000085u and <= 0x10000093u => identity.ActionId - 0x10000071u,
|
||||
0x1000010Eu => 0x23u,
|
||||
0x1000010Fu => 0x24u,
|
||||
0x10000110u => 0x25u,
|
||||
0x10000112u => 0x26u,
|
||||
0x1000011Bu => 0x28u,
|
||||
0x1000011Du => 0x29u,
|
||||
0x1000011Eu => 0x2Au,
|
||||
0x1000011Fu => 0x2Bu,
|
||||
0x10000120u => 0x2Cu,
|
||||
0x10000123u => 0x2Du,
|
||||
0x10000125u => 0x2Eu,
|
||||
0x1000012Au => 0x2Fu,
|
||||
0x1000012Cu => 0x30u,
|
||||
0x1000012Fu => 0x32u,
|
||||
0x1000013Eu => 0x13u,
|
||||
_ => uint.MaxValue,
|
||||
};
|
||||
return optionId != uint.MaxValue;
|
||||
}
|
||||
|
||||
private static Dictionary<(uint, uint), InputAction> BuildTable()
|
||||
{
|
||||
var t = new Dictionary<(uint, uint), InputAction>();
|
||||
|
|
@ -89,24 +155,9 @@ public static class RetailActionIdentityTable
|
|||
M(0x4, 0x10000097, InputAction.Sleeping);
|
||||
|
||||
// ── CameraControls (ctx 0x5) — 12/12. ──────────────────────────
|
||||
// M2 REWORK (2026-08-11 review): CameraControls (ctx 0x5, the
|
||||
// Numpad-default scheme RetailDefaults() actually carries) and
|
||||
// CameraAlternateControls (ctx 0x6, the arrow-key alternate scheme
|
||||
// RetailDefaults() never had — see
|
||||
// RetailActionIdentityRoundTripTests' now-retired camera allowlist
|
||||
// entries) were both previously mapped to the SAME InputAction.
|
||||
// KeyBindings/Binding has no "which scheme" tag, and SetForAction is
|
||||
// whole-action replacement, so the two rows aliased one live target:
|
||||
// both showed identical (stale) chords, rebinding one silently wiped
|
||||
// the other, and a row could conflict with its own twin. Building
|
||||
// real per-scheme dual-binding storage (or ten new InputAction
|
||||
// members plus the camera-dispatch code to consume them) is a real
|
||||
// feature, not a one-line fix, and out of scope for this rework. Only
|
||||
// ctx 0x5 — the scheme that already has a live, verified
|
||||
// RetailDefaults() presence — maps here; ctx 0x6 falls through to the
|
||||
// generic unmapped/store-only path below (AP-203), fully renderable,
|
||||
// bindable and persisted, honestly carrying no live effect, exactly
|
||||
// like every other unmapped row.
|
||||
// The primary and alternate maps deliberately use distinct actions.
|
||||
// Retail reuses the ten low action ids, but they are separate rows and
|
||||
// separate rebind targets; collapsing them aliases GUI state.
|
||||
M(0x5, 0x33, InputAction.CameraMoveToward);
|
||||
M(0x5, 0x34, InputAction.CameraMoveAway);
|
||||
M(0x5, 0x35, InputAction.CameraRotateLeft);
|
||||
|
|
@ -120,6 +171,18 @@ public static class RetailActionIdentityTable
|
|||
M(0x5, 0x3D, InputAction.CameraInstantMouseLook);
|
||||
M(0x5, 0x3E, InputAction.CameraActivateAlternateMode);
|
||||
|
||||
// ── CameraAlternateControls (ctx 0x6) — 10/10. ─────────────
|
||||
M(0x6, 0x33, InputAction.CameraAlternateMoveToward);
|
||||
M(0x6, 0x34, InputAction.CameraAlternateMoveAway);
|
||||
M(0x6, 0x35, InputAction.CameraAlternateRotateLeft);
|
||||
M(0x6, 0x36, InputAction.CameraAlternateRotateRight);
|
||||
M(0x6, 0x37, InputAction.CameraAlternateRotateUp);
|
||||
M(0x6, 0x38, InputAction.CameraAlternateRotateDown);
|
||||
M(0x6, 0x39, InputAction.CameraAlternateViewDefault);
|
||||
M(0x6, 0x3A, InputAction.CameraAlternateViewFirstPerson);
|
||||
M(0x6, 0x3B, InputAction.CameraAlternateViewLookDown);
|
||||
M(0x6, 0x3C, InputAction.CameraAlternateViewMapMode);
|
||||
|
||||
// ── Combat (ctx 0x10000002) — 1/1. ─────────────────────────────
|
||||
M(0x10000002, 0x1000005A, InputAction.CombatToggleCombat);
|
||||
|
||||
|
|
@ -137,8 +200,7 @@ public static class RetailActionIdentityTable
|
|||
M(0x10000004, 0x100000F2, InputAction.CombatAimMedium);
|
||||
M(0x10000004, 0x100000F3, InputAction.CombatAimHigh);
|
||||
|
||||
// ── MagicCombat (ctx 0x10000005) — 18/21 (Spell Slot 10/11/12 have
|
||||
// no InputAction — register row). ────────────────────────────
|
||||
// ── MagicCombat (ctx 0x10000005) — 21/21. ──────────────────────
|
||||
M(0x10000005, 0x10000060, InputAction.CombatCastCurrentSpell);
|
||||
M(0x10000005, 0x10000061, InputAction.CombatPrevSpell);
|
||||
M(0x10000005, 0x10000062, InputAction.CombatNextSpell);
|
||||
|
|
@ -153,21 +215,111 @@ public static class RetailActionIdentityTable
|
|||
M(0x10000005, 0x1000006B, InputAction.UseSpellSlot_7);
|
||||
M(0x10000005, 0x1000006C, InputAction.UseSpellSlot_8);
|
||||
M(0x10000005, 0x1000006D, InputAction.UseSpellSlot_9);
|
||||
// 0x6E/0x6F/0x70 (Spell Slot 10/11/12) — no InputAction. Unmapped.
|
||||
M(0x10000005, 0x1000006E, InputAction.UseSpellSlot_10);
|
||||
M(0x10000005, 0x1000006F, InputAction.UseSpellSlot_11);
|
||||
M(0x10000005, 0x10000070, InputAction.UseSpellSlot_12);
|
||||
M(0x10000005, 0x10000102, InputAction.CombatFirstSpell);
|
||||
M(0x10000005, 0x10000103, InputAction.CombatLastSpell);
|
||||
M(0x10000005, 0x10000104, InputAction.CombatFirstSpellTab);
|
||||
M(0x10000005, 0x10000105, InputAction.CombatLastSpellTab);
|
||||
|
||||
// ── Emotes (ctx 0x10000006) — 5/87 (the only 5 acdream dispatches
|
||||
// an animation for; also the only 5 with retail default keys). ──
|
||||
M(0x10000006, 0x100000A2, InputAction.Cheer);
|
||||
M(0x10000006, 0x100000A7, InputAction.Cry);
|
||||
M(0x10000006, 0x100000B2, InputAction.Laugh);
|
||||
M(0x10000006, 0x100000BE, InputAction.PointState);
|
||||
M(0x10000006, 0x100000E5, InputAction.Wave);
|
||||
// ── Emotes (ctx 0x10000006) — 87/87. ────────────────────────
|
||||
InputAction[] emotes =
|
||||
{
|
||||
InputAction.EmoteAfkState,
|
||||
InputAction.EmoteAkimbo,
|
||||
InputAction.EmoteAToyotState,
|
||||
InputAction.EmoteAkimboState,
|
||||
InputAction.EmoteAtEaseState,
|
||||
InputAction.EmoteBeckon,
|
||||
InputAction.EmoteBeSeeingYou,
|
||||
InputAction.EmoteBlowKiss,
|
||||
InputAction.EmoteBowDeep,
|
||||
InputAction.EmoteBowDeepState,
|
||||
InputAction.Cheer,
|
||||
InputAction.EmoteClapHands,
|
||||
InputAction.EmoteClapHandsState,
|
||||
InputAction.EmoteCringe,
|
||||
InputAction.EmoteCrossArmsState,
|
||||
InputAction.Cry,
|
||||
InputAction.EmoteCurtseyState,
|
||||
InputAction.EmoteDrudgeDance,
|
||||
InputAction.EmoteDrudgeDanceState,
|
||||
InputAction.EmoteHaveASeat,
|
||||
InputAction.EmoteHaveASeatState,
|
||||
InputAction.EmoteHeartyLaugh,
|
||||
InputAction.EmoteHelper,
|
||||
InputAction.EmoteKneel,
|
||||
InputAction.EmoteKneelState,
|
||||
InputAction.EmoteKnock,
|
||||
InputAction.Laugh,
|
||||
InputAction.EmoteLeanState,
|
||||
InputAction.EmoteMeditateState,
|
||||
InputAction.EmoteMimeDrinking,
|
||||
InputAction.EmoteMimeEating,
|
||||
InputAction.EmoteMock,
|
||||
InputAction.EmoteNod,
|
||||
InputAction.EmoteNudgeLeft,
|
||||
InputAction.EmoteNudgeRight,
|
||||
InputAction.EmotePlead,
|
||||
InputAction.EmotePleadState,
|
||||
InputAction.EmotePoint,
|
||||
InputAction.PointState,
|
||||
InputAction.EmotePointDown,
|
||||
InputAction.EmotePointDownState,
|
||||
InputAction.EmotePointLeft,
|
||||
InputAction.EmotePointLeftState,
|
||||
InputAction.EmotePointRight,
|
||||
InputAction.EmotePointRightState,
|
||||
InputAction.EmotePossumState,
|
||||
InputAction.EmotePray,
|
||||
InputAction.EmotePrayState,
|
||||
InputAction.EmoteReadState,
|
||||
InputAction.EmoteSalute,
|
||||
InputAction.EmoteSaluteState,
|
||||
InputAction.EmoteScanHorizon,
|
||||
InputAction.EmoteScratchHead,
|
||||
InputAction.EmoteScratchHeadState,
|
||||
InputAction.EmoteShakeFist,
|
||||
InputAction.EmoteShakeFistState,
|
||||
InputAction.EmoteShakeHead,
|
||||
InputAction.EmoteShiver,
|
||||
InputAction.EmoteShiverState,
|
||||
InputAction.EmoteShoo,
|
||||
InputAction.EmoteShrug,
|
||||
InputAction.EmoteSitState,
|
||||
InputAction.EmoteSitBackState,
|
||||
InputAction.EmoteSitCrossleggedState,
|
||||
InputAction.EmoteSlouch,
|
||||
InputAction.EmoteSlouchState,
|
||||
InputAction.EmoteSmackHead,
|
||||
InputAction.EmoteSnowAngelState,
|
||||
InputAction.EmoteSpit,
|
||||
InputAction.EmoteSurrender,
|
||||
InputAction.EmoteSurrenderState,
|
||||
InputAction.EmoteTalkToTheHandState,
|
||||
InputAction.EmoteTapFoot,
|
||||
InputAction.EmoteTapFootState,
|
||||
InputAction.EmoteTeapot,
|
||||
InputAction.EmoteThinkerState,
|
||||
InputAction.EmoteWarmHands,
|
||||
InputAction.Wave,
|
||||
InputAction.EmoteWaveState,
|
||||
InputAction.EmoteWaveLow,
|
||||
InputAction.EmoteWaveHigh,
|
||||
InputAction.EmoteWinded,
|
||||
InputAction.EmoteWindedState,
|
||||
InputAction.EmoteWoah,
|
||||
InputAction.EmoteWoahState,
|
||||
InputAction.EmoteYawnAndStretch,
|
||||
InputAction.EmoteYmca,
|
||||
};
|
||||
for (int i = 0; i < emotes.Length; i++)
|
||||
M(0x10000006, 0x10000098u + (uint)i, emotes[i]);
|
||||
|
||||
// ── ItemSelectionCommands (ctx 0x10000007) — 17/26. ────────────
|
||||
// ── ItemSelectionCommands (ctx 0x10000007) — 26/26. ────────────
|
||||
M(0x10000007, 0x1000002A, InputAction.SelectionSelf);
|
||||
M(0x10000007, 0x1000002C, InputAction.SelectionPlaceInInventory);
|
||||
M(0x10000007, 0x1000002D, InputAction.SelectionSplitStack);
|
||||
M(0x10000007, 0x1000002E, InputAction.SelectionPreviousSelection);
|
||||
M(0x10000007, 0x1000002F, InputAction.SelectionClosestCompassItem);
|
||||
|
|
@ -185,20 +337,44 @@ public static class RetailActionIdentityTable
|
|||
M(0x10000007, 0x1000003B, InputAction.SelectionNextPlayer);
|
||||
M(0x10000007, 0x1000003C, InputAction.SelectionPreviousFellow);
|
||||
M(0x10000007, 0x1000003D, InputAction.SelectionNextFellow);
|
||||
M(0x10000007, 0x1000003E, InputAction.SelectionUseClosestUnopenedCorpse);
|
||||
M(0x10000007, 0x1000003F, InputAction.SelectionUseNextUnopenedCorpse);
|
||||
M(0x10000007, 0x10000040, InputAction.SelectionGiveToTarget);
|
||||
M(0x10000007, 0x10000041, InputAction.SelectionDrop);
|
||||
M(0x10000007, 0x1000011C, InputAction.SelectionPlaceInMainPack);
|
||||
M(0x10000007, 0x10000121, InputAction.SelectionClosestUnopenedCorpse);
|
||||
M(0x10000007, 0x10000122, InputAction.SelectionNextUnopenedCorpse);
|
||||
|
||||
// ── UICommands (ctx 0x10000009) — 22/42. ───────────────────────
|
||||
// ── UICommands (ctx 0x10000009) — 42/42. ───────────────────────
|
||||
M(0x10000009, 0x55, InputAction.CaptureScreenshot);
|
||||
M(0x10000009, 0x7B, InputAction.ToggleHelp);
|
||||
M(0x10000009, 0x7C, InputAction.TogglePluginManager);
|
||||
M(0x10000009, 0x10000003, InputAction.ToggleAbuseReportingPanel);
|
||||
M(0x10000009, 0x10000005, InputAction.ToggleCharacterInfoPanel);
|
||||
M(0x10000009, 0x10000006, InputAction.TogglePositiveMagicPanel);
|
||||
M(0x10000009, 0x10000007, InputAction.ToggleNegativeMagicPanel);
|
||||
M(0x10000009, 0x10000009, InputAction.ToggleLinkStatusPanel);
|
||||
M(0x10000009, 0x1000000B, InputAction.ToggleUrgentAssistancePanel);
|
||||
M(0x10000009, 0x1000000C, InputAction.ToggleVitaePanel);
|
||||
M(0x10000009, 0x1000000D, InputAction.ToggleSocialPanel);
|
||||
M(0x10000009, 0x1000000E, InputAction.ToggleAllegiancePanel);
|
||||
M(0x10000009, 0x1000000F, InputAction.ToggleFellowshipPanel);
|
||||
M(0x10000009, 0x10000010, InputAction.ToggleSpellManagementPanel);
|
||||
M(0x10000009, 0x10000011, InputAction.ToggleSpellbookPanel);
|
||||
M(0x10000009, 0x10000012, InputAction.ToggleSpellComponentsPanel);
|
||||
M(0x10000009, 0x10000013, InputAction.ToggleCharacterDetailPanel);
|
||||
M(0x10000009, 0x10000014, InputAction.ToggleAttributesPanel);
|
||||
M(0x10000009, 0x10000015, InputAction.ToggleSkillsPanel);
|
||||
M(0x10000009, 0x10000016, InputAction.ToggleWorldPanel);
|
||||
M(0x10000009, 0x10000017, InputAction.ToggleMapPage);
|
||||
M(0x10000009, 0x10000018, InputAction.ToggleHousePage);
|
||||
M(0x10000009, 0x1000001A, InputAction.ToggleOptionsPanel);
|
||||
M(0x10000009, 0x10000019, InputAction.ToggleInventoryPanel);
|
||||
M(0x10000009, 0x1000001B, InputAction.ToggleGameplayOptionsPage);
|
||||
M(0x10000009, 0x1000001C, InputAction.ToggleCharacterSettingsPage);
|
||||
M(0x10000009, 0x1000001D, InputAction.ToggleConfigurationPage);
|
||||
M(0x10000009, 0x1000001E, InputAction.ToggleCompass);
|
||||
M(0x10000009, 0x1000001F, InputAction.ToggleKeyboardConfiguration);
|
||||
M(0x10000009, 0x10000114, InputAction.ToggleFloatingChatWindow1);
|
||||
M(0x10000009, 0x10000115, InputAction.ToggleFloatingChatWindow2);
|
||||
M(0x10000009, 0x10000116, InputAction.ToggleFloatingChatWindow3);
|
||||
|
|
@ -206,19 +382,24 @@ public static class RetailActionIdentityTable
|
|||
M(0x10000009, 0x10000025, InputAction.UseSelected);
|
||||
M(0x10000009, 0x10000026, InputAction.LOGOUT);
|
||||
M(0x10000009, 0x1000002B, InputAction.SelectionExamine);
|
||||
// 0x1000001F ("Show/Hide Keyboard Configuration") deliberately left
|
||||
// unmapped: it is the retail action that opens THIS screen
|
||||
// (research doc §4.3/lane A §7 — wired directly by
|
||||
// KeyboardConfigController's mount, not through InputAction).
|
||||
|
||||
// ── ChatCommands (ctx 0x1000000A) — 1/6. ───────────────────────
|
||||
M(0x10000009, 0x10000118, InputAction.ToggleFriendsPage);
|
||||
M(0x10000009, 0x1000011A, InputAction.ToggleCharacterTitlesPage);
|
||||
M(0x10000009, 0x10000127, InputAction.ToggleQuestDetailPanel);
|
||||
M(0x10000009, 0x10000128, InputAction.ToggleQuestJournalPage);
|
||||
M(0x10000009, 0x10000129, InputAction.ToggleJournalPageList);
|
||||
M(0x10000009, 0x1000012E, InputAction.ToggleContractsPage);
|
||||
// ── ChatCommands (ctx 0x1000000A) — 6/6. ───────────────────────
|
||||
M(0x1000000A, 0x10000020, InputAction.ChatMonarchReply);
|
||||
M(0x1000000A, 0x10000021, InputAction.ChatPatronReply);
|
||||
M(0x1000000A, 0x10000022, InputAction.ChatReply);
|
||||
M(0x1000000A, 0x10000023, InputAction.EnterChatMode);
|
||||
M(0x1000000A, 0x10000028, InputAction.ChatStartCommand);
|
||||
M(0x1000000A, 0x10000119, InputAction.ChatTellToSelected);
|
||||
|
||||
// ── ToggleChatEntry (ctx 0x1000000D) — 1/1. ────────────────────
|
||||
M(0x1000000D, 0x10000024, InputAction.ToggleChatEntry);
|
||||
|
||||
// ── QuickslotCommands (ctx 0x1000000C) — 24/28 (Quickslot
|
||||
// 10/11/12/13 have no InputAction — pre-existing enum gap). ──
|
||||
// ── QuickslotCommands (ctx 0x1000000C) — 28/28. ────────────────
|
||||
M(0x1000000C, 0x10000042, InputAction.UseQuickSlot_1);
|
||||
M(0x1000000C, 0x10000043, InputAction.UseQuickSlot_2);
|
||||
M(0x1000000C, 0x10000044, InputAction.UseQuickSlot_3);
|
||||
|
|
@ -228,6 +409,9 @@ public static class RetailActionIdentityTable
|
|||
M(0x1000000C, 0x10000048, InputAction.UseQuickSlot_7);
|
||||
M(0x1000000C, 0x10000049, InputAction.UseQuickSlot_8);
|
||||
M(0x1000000C, 0x1000004A, InputAction.UseQuickSlot_9);
|
||||
M(0x1000000C, 0x1000004B, InputAction.UseQuickSlot_10);
|
||||
M(0x1000000C, 0x1000004C, InputAction.UseQuickSlot_11);
|
||||
M(0x1000000C, 0x1000004D, InputAction.UseQuickSlot_12);
|
||||
M(0x1000000C, 0x1000004E, InputAction.SelectQuickSlot_1);
|
||||
M(0x1000000C, 0x1000004F, InputAction.SelectQuickSlot_2);
|
||||
M(0x1000000C, 0x10000050, InputAction.SelectQuickSlot_3);
|
||||
|
|
@ -238,16 +422,62 @@ public static class RetailActionIdentityTable
|
|||
M(0x1000000C, 0x10000055, InputAction.SelectQuickSlot_8);
|
||||
M(0x1000000C, 0x10000056, InputAction.SelectQuickSlot_9);
|
||||
M(0x1000000C, 0x1000010D, InputAction.CreateShortcut);
|
||||
// 0x10000132 ("Quickslot 13") has no InputAction — same pre-existing
|
||||
// UseQuickSlot_10..13 enum gap as the bare-numeral block above. Unmapped.
|
||||
M(0x1000000C, 0x10000132, InputAction.UseQuickSlot_13);
|
||||
M(0x1000000C, 0x10000133, InputAction.UseQuickSlot_14);
|
||||
M(0x1000000C, 0x10000134, InputAction.UseQuickSlot_15);
|
||||
M(0x1000000C, 0x10000135, InputAction.UseQuickSlot_16);
|
||||
M(0x1000000C, 0x10000136, InputAction.UseQuickSlot_17);
|
||||
M(0x1000000C, 0x10000137, InputAction.UseQuickSlot_18);
|
||||
|
||||
// CharacterSettings (ctx 0x10000008) is intentionally EMPTY here —
|
||||
// see class doc "Known gaps deliberately left unmapped".
|
||||
// ── CharacterSettings (ctx 0x10000008) — 48/48. ────────────────
|
||||
M(0x10000008, 0x10000071, InputAction.ToggleCharacterOptionAutoRepeatAttack);
|
||||
M(0x10000008, 0x10000072, InputAction.ToggleCharacterOptionIgnoreAllegianceRequests);
|
||||
M(0x10000008, 0x10000073, InputAction.ToggleCharacterOptionIgnoreFellowshipRequests);
|
||||
M(0x10000008, 0x10000074, InputAction.ToggleCharacterOptionIgnoreTradeRequests);
|
||||
M(0x10000008, 0x10000076, InputAction.ToggleCharacterOptionPersistentAtDay);
|
||||
M(0x10000008, 0x10000077, InputAction.ToggleCharacterOptionAllowGive);
|
||||
M(0x10000008, 0x10000078, InputAction.ToggleCharacterOptionViewCombatTarget);
|
||||
M(0x10000008, 0x10000079, InputAction.ToggleCharacterOptionShowTooltips);
|
||||
M(0x10000008, 0x1000007A, InputAction.ToggleCharacterOptionUseDeception);
|
||||
M(0x10000008, 0x1000007B, InputAction.ToggleCharacterOptionToggleRun);
|
||||
M(0x10000008, 0x1000007C, InputAction.ToggleCharacterOptionStayInChatMode);
|
||||
M(0x10000008, 0x1000007D, InputAction.ToggleCharacterOptionAdvancedCombatUi);
|
||||
M(0x10000008, 0x1000007E, InputAction.ToggleCharacterOptionAutoTarget);
|
||||
M(0x10000008, 0x1000007F, InputAction.ToggleCharacterOptionVividTargetingIndicator);
|
||||
M(0x10000008, 0x10000080, InputAction.ToggleCharacterOptionFellowshipShareXp);
|
||||
M(0x10000008, 0x10000081, InputAction.ToggleCharacterOptionAcceptLootPermits);
|
||||
M(0x10000008, 0x10000082, InputAction.ToggleCharacterOptionFellowshipShareLoot);
|
||||
M(0x10000008, 0x10000083, InputAction.ToggleCharacterOptionFellowshipAutoAcceptRequests);
|
||||
M(0x10000008, 0x10000085, InputAction.ToggleCharacterOptionCoordinatesOnRadar);
|
||||
M(0x10000008, 0x10000086, InputAction.ToggleCharacterOptionSpellDuration);
|
||||
M(0x10000008, 0x10000087, InputAction.ToggleCharacterOptionDisableHouseRestrictionEffects);
|
||||
M(0x10000008, 0x10000088, InputAction.ToggleCharacterOptionDragItemOnPlayerOpensSecureTrade);
|
||||
M(0x10000008, 0x10000089, InputAction.ToggleCharacterOptionDisplayAllegianceLogonNotifications);
|
||||
M(0x10000008, 0x1000008A, InputAction.ToggleCharacterOptionUseChargeAttack);
|
||||
M(0x10000008, 0x1000008B, InputAction.ToggleCharacterOptionUseCraftSuccessDialog);
|
||||
M(0x10000008, 0x1000008C, InputAction.ToggleCharacterOptionListenToAllegianceChat);
|
||||
M(0x10000008, 0x1000008D, InputAction.ToggleCharacterOptionDisplayDateOfBirth);
|
||||
M(0x10000008, 0x1000008E, InputAction.ToggleCharacterOptionDisplayAge);
|
||||
M(0x10000008, 0x1000008F, InputAction.ToggleCharacterOptionDisplayChessRank);
|
||||
M(0x10000008, 0x10000090, InputAction.ToggleCharacterOptionDisplayFishingSkill);
|
||||
M(0x10000008, 0x10000091, InputAction.ToggleCharacterOptionDisplayNumberDeaths);
|
||||
M(0x10000008, 0x10000092, InputAction.ToggleCharacterOptionDisplayTimeStamps);
|
||||
M(0x10000008, 0x10000093, InputAction.ToggleCharacterOptionSalvageMultiple);
|
||||
M(0x10000008, 0x1000010E, InputAction.ToggleCharacterOptionListenToGeneralChat);
|
||||
M(0x10000008, 0x1000010F, InputAction.ToggleCharacterOptionListenToTradeChat);
|
||||
M(0x10000008, 0x10000110, InputAction.ToggleCharacterOptionListenToLfgChat);
|
||||
M(0x10000008, 0x10000112, InputAction.ToggleCharacterOptionListenToRoleplayChat);
|
||||
M(0x10000008, 0x1000011B, InputAction.ToggleCharacterOptionDisplayNumberCharacterTitles);
|
||||
M(0x10000008, 0x1000011D, InputAction.ToggleCharacterOptionMainPackPreferred);
|
||||
M(0x10000008, 0x1000011E, InputAction.ToggleCharacterOptionLeadMissileTargets);
|
||||
M(0x10000008, 0x1000011F, InputAction.ToggleCharacterOptionUseFastMissiles);
|
||||
M(0x10000008, 0x10000120, InputAction.ToggleCharacterOptionFilterLanguage);
|
||||
M(0x10000008, 0x10000123, InputAction.ToggleCharacterOptionConfirmVolatileRareUse);
|
||||
M(0x10000008, 0x10000125, InputAction.ToggleCharacterOptionListenToSocietyChat);
|
||||
M(0x10000008, 0x1000012A, InputAction.ToggleCharacterOptionShowHelm);
|
||||
M(0x10000008, 0x1000012C, InputAction.ToggleCharacterOptionDisableDistanceFog);
|
||||
M(0x10000008, 0x1000012F, InputAction.ToggleCharacterOptionShowCloak);
|
||||
M(0x10000008, 0x1000013E, InputAction.ToggleCharacterOptionSideBySideVitals);
|
||||
|
||||
return t;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ namespace AcDream.UI.Abstractions.Input;
|
|||
/// <see cref="Key"/> enum.
|
||||
///
|
||||
/// <para>
|
||||
/// The scan-code table covers exactly the 84 distinct DIK codes that appear across
|
||||
/// the DAT's 306 user-bindable ActionMap rows' default bindings (2026-08-11 probe —
|
||||
/// see <c>RetailActionMap.cs</c>'s class doc), cross-checked against
|
||||
/// The scan-code table covers the 84 distinct DIK codes that appear across the
|
||||
/// DAT's 306 user-bindable ActionMap rows' default bindings (2026-08-11 probe),
|
||||
/// plus the remaining keyboard controls accepted by retail's plain-text keymap
|
||||
/// interchange. The default set was cross-checked against
|
||||
/// <c>tools/dump-keymap/Program.cs</c>'s own <c>Dik(uint)</c> transcription (itself
|
||||
/// verified against <c>acclient_2013_pseudo_c.txt</c>'s
|
||||
/// <c>ControlNameMapper::AddKeySemantic</c> calls) and against
|
||||
/// <see cref="KeyBindings.RetailDefaults"/>'s existing chords, which already encode
|
||||
/// the same standard US-layout DirectInput scan codes by construction (both were
|
||||
/// authored from the same <c>retail-default.keymap.txt</c>). Codes outside this set
|
||||
/// (rare/debug/joystick bindings never seen with a non-empty default in the shipped
|
||||
/// DAT) intentionally return null rather than guess.
|
||||
/// authored from the same <c>retail-default.keymap.txt</c>). Unsupported joystick
|
||||
/// controls intentionally return null rather than guess.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailScanCodeMap
|
||||
|
|
@ -94,6 +94,7 @@ public static class RetailScanCodeMap
|
|||
0x1A => Key.LeftBracket,
|
||||
0x1B => Key.RightBracket,
|
||||
0x1C => Key.Enter,
|
||||
0x1D => Key.ControlLeft,
|
||||
0x1E => Key.A,
|
||||
0x1F => Key.S,
|
||||
0x20 => Key.D,
|
||||
|
|
@ -120,7 +121,9 @@ public static class RetailScanCodeMap
|
|||
0x35 => Key.Slash,
|
||||
0x36 => Key.ShiftRight,
|
||||
0x37 => Key.KeypadMultiply,
|
||||
0x38 => Key.AltLeft,
|
||||
0x39 => Key.Space,
|
||||
0x3A => Key.CapsLock,
|
||||
0x3B => Key.F1,
|
||||
0x3C => Key.F2,
|
||||
0x3D => Key.F3,
|
||||
|
|
@ -148,9 +151,15 @@ public static class RetailScanCodeMap
|
|||
0x53 => Key.KeypadDecimal,
|
||||
0x57 => Key.F11,
|
||||
0x58 => Key.F12,
|
||||
0x64 => Key.F13,
|
||||
0x65 => Key.F14,
|
||||
0x66 => Key.F15,
|
||||
0x9C => Key.KeypadEnter,
|
||||
0x9D => Key.ControlRight,
|
||||
0xB5 => Key.KeypadDivide,
|
||||
0xB7 => Key.PrintScreen,
|
||||
0xB8 => Key.AltRight,
|
||||
0xC5 => Key.Pause,
|
||||
0xC7 => Key.Home,
|
||||
0xC8 => Key.Up,
|
||||
0xC9 => Key.PageUp,
|
||||
|
|
@ -161,7 +170,163 @@ public static class RetailScanCodeMap
|
|||
0xD1 => Key.PageDown,
|
||||
0xD2 => Key.Insert,
|
||||
0xD3 => Key.Delete,
|
||||
0xDB => Key.SuperLeft,
|
||||
0xDC => Key.SuperRight,
|
||||
0xDD => Key.Menu,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail's plain-text <c>.keymap</c> control semantic to the same device /
|
||||
/// scan-code pair consumed by <see cref="ToSilkKey"/>. The legacy file
|
||||
/// uses a few historical aliases (<c>UPARROW</c>, <c>PGUP</c>,
|
||||
/// <c>NUMPADSTAR</c>, ...), so parsing accepts both those spellings and the
|
||||
/// canonical DirectInput spellings emitted by <see cref="TryToFileControl"/>.
|
||||
/// </summary>
|
||||
public static bool TryFromFileControl(
|
||||
string control,
|
||||
out uint scan,
|
||||
out uint device)
|
||||
{
|
||||
scan = 0u;
|
||||
device = 0u;
|
||||
if (string.IsNullOrWhiteSpace(control))
|
||||
return false;
|
||||
|
||||
string token = control.Trim().ToUpperInvariant();
|
||||
if (token.StartsWith("DIMOFS_BUTTON", StringComparison.Ordinal)
|
||||
&& int.TryParse(token["DIMOFS_BUTTON".Length..], out int button)
|
||||
&& button is >= 0 and <= 4)
|
||||
{
|
||||
scan = (uint)(0x0C + button);
|
||||
device = 1u;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!token.StartsWith("DIK_", StringComparison.Ordinal))
|
||||
return false;
|
||||
token = token[4..];
|
||||
scan = token switch
|
||||
{
|
||||
"ESCAPE" => 0x01,
|
||||
"1" => 0x02, "2" => 0x03, "3" => 0x04, "4" => 0x05,
|
||||
"5" => 0x06, "6" => 0x07, "7" => 0x08, "8" => 0x09,
|
||||
"9" => 0x0A, "0" => 0x0B,
|
||||
"MINUS" => 0x0C, "EQUALS" => 0x0D, "BACK" => 0x0E,
|
||||
"TAB" => 0x0F,
|
||||
"Q" => 0x10, "W" => 0x11, "E" => 0x12, "R" => 0x13,
|
||||
"T" => 0x14, "Y" => 0x15, "U" => 0x16, "I" => 0x17,
|
||||
"O" => 0x18, "P" => 0x19,
|
||||
"LBRACKET" => 0x1A, "RBRACKET" => 0x1B, "RETURN" => 0x1C,
|
||||
"LCONTROL" => 0x1D,
|
||||
"A" => 0x1E, "S" => 0x1F, "D" => 0x20, "F" => 0x21,
|
||||
"G" => 0x22, "H" => 0x23, "J" => 0x24, "K" => 0x25,
|
||||
"L" => 0x26, "SEMICOLON" => 0x27, "APOSTROPHE" => 0x28,
|
||||
"GRAVE" => 0x29, "LSHIFT" => 0x2A, "BACKSLASH" => 0x2B,
|
||||
"Z" => 0x2C, "X" => 0x2D, "C" => 0x2E, "V" => 0x2F,
|
||||
"B" => 0x30, "N" => 0x31, "M" => 0x32,
|
||||
"COMMA" => 0x33, "PERIOD" => 0x34, "SLASH" => 0x35,
|
||||
"RSHIFT" => 0x36, "MULTIPLY" or "NUMPADSTAR" => 0x37,
|
||||
"LMENU" or "LALT" => 0x38, "SPACE" => 0x39, "CAPITAL" => 0x3A,
|
||||
"F1" => 0x3B, "F2" => 0x3C, "F3" => 0x3D, "F4" => 0x3E,
|
||||
"F5" => 0x3F, "F6" => 0x40, "F7" => 0x41, "F8" => 0x42,
|
||||
"F9" => 0x43, "F10" => 0x44, "NUMLOCK" => 0x45,
|
||||
"SCROLL" => 0x46, "NUMPAD7" => 0x47, "NUMPAD8" => 0x48,
|
||||
"NUMPAD9" => 0x49, "SUBTRACT" or "NUMPADMINUS" => 0x4A,
|
||||
"NUMPAD4" => 0x4B, "NUMPAD5" => 0x4C, "NUMPAD6" => 0x4D,
|
||||
"ADD" or "NUMPADPLUS" => 0x4E, "NUMPAD1" => 0x4F,
|
||||
"NUMPAD2" => 0x50, "NUMPAD3" => 0x51, "NUMPAD0" => 0x52,
|
||||
"DECIMAL" or "NUMPADPERIOD" => 0x53,
|
||||
"F11" => 0x57, "F12" => 0x58, "F13" => 0x64,
|
||||
"F14" => 0x65, "F15" => 0x66, "NUMPADENTER" => 0x9C,
|
||||
"RCONTROL" => 0x9D, "DIVIDE" or "NUMPADSLASH" => 0xB5,
|
||||
"SYSRQ" => 0xB7, "RMENU" or "RALT" => 0xB8,
|
||||
"PAUSE" => 0xC5, "HOME" => 0xC7,
|
||||
"UP" or "UPARROW" => 0xC8, "PRIOR" or "PGUP" => 0xC9,
|
||||
"LEFT" => 0xCB, "RIGHT" or "RIGHTARROW" => 0xCD,
|
||||
"END" => 0xCF, "DOWN" or "DOWNARROW" => 0xD0,
|
||||
"NEXT" or "PGDN" => 0xD1, "INSERT" => 0xD2,
|
||||
"DELETE" => 0xD3, "LWIN" => 0xDB, "RWIN" => 0xDC,
|
||||
"APPS" => 0xDD,
|
||||
_ => uint.MaxValue,
|
||||
};
|
||||
return scan != uint.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>Converts an acdream chord to retail's plain-text control
|
||||
/// semantic. Returns false for controls the 2013 DirectInput keymap cannot
|
||||
/// represent (for example joystick axes).</summary>
|
||||
public static bool TryToFileControl(KeyChord chord, out string control)
|
||||
{
|
||||
control = string.Empty;
|
||||
if (chord.Device == 1)
|
||||
{
|
||||
int button = (int)chord.Key switch
|
||||
{
|
||||
-1001 => 0,
|
||||
-1002 => 1,
|
||||
-1003 => 2,
|
||||
-1004 => 3,
|
||||
-1005 => 4,
|
||||
_ => -1,
|
||||
};
|
||||
if (button < 0) return false;
|
||||
control = $"DIMOFS_BUTTON{button}";
|
||||
return true;
|
||||
}
|
||||
if (chord.Device != 0) return false;
|
||||
|
||||
for (uint scan = 1; scan <= 0xDD; scan++)
|
||||
{
|
||||
if (ToSilkKey(scan, 0) != chord.Key) continue;
|
||||
control = scan switch
|
||||
{
|
||||
0x37 => "DIK_NUMPADSTAR",
|
||||
0x4A => "DIK_NUMPADMINUS",
|
||||
0x4E => "DIK_NUMPADPLUS",
|
||||
0xB5 => "DIK_NUMPADSLASH",
|
||||
0xC8 => "DIK_UPARROW",
|
||||
0xC9 => "DIK_PGUP",
|
||||
0xCD => "DIK_RIGHTARROW",
|
||||
0xD0 => "DIK_DOWNARROW",
|
||||
0xD1 => "DIK_PGDN",
|
||||
_ => FileToken(scan),
|
||||
};
|
||||
return control.Length != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string FileToken(uint scan) => scan switch
|
||||
{
|
||||
0x01 => "DIK_ESCAPE",
|
||||
>= 0x02 and <= 0x0A => $"DIK_{scan - 1}",
|
||||
0x0B => "DIK_0", 0x0C => "DIK_MINUS", 0x0D => "DIK_EQUALS",
|
||||
0x0E => "DIK_BACK", 0x0F => "DIK_TAB",
|
||||
>= 0x10 and <= 0x19 => $"DIK_{"QWERTYUIOP"[(int)(scan - 0x10)]}",
|
||||
0x1A => "DIK_LBRACKET", 0x1B => "DIK_RBRACKET",
|
||||
0x1C => "DIK_RETURN", 0x1D => "DIK_LCONTROL",
|
||||
>= 0x1E and <= 0x26 => $"DIK_{"ASDFGHJKL"[(int)(scan - 0x1E)]}",
|
||||
0x27 => "DIK_SEMICOLON", 0x28 => "DIK_APOSTROPHE",
|
||||
0x29 => "DIK_GRAVE", 0x2A => "DIK_LSHIFT", 0x2B => "DIK_BACKSLASH",
|
||||
>= 0x2C and <= 0x32 => $"DIK_{"ZXCVBNM"[(int)(scan - 0x2C)]}",
|
||||
0x33 => "DIK_COMMA", 0x34 => "DIK_PERIOD", 0x35 => "DIK_SLASH",
|
||||
0x36 => "DIK_RSHIFT", 0x38 => "DIK_LMENU", 0x39 => "DIK_SPACE",
|
||||
0x3A => "DIK_CAPITAL",
|
||||
>= 0x3B and <= 0x44 => $"DIK_F{scan - 0x3A}",
|
||||
0x45 => "DIK_NUMLOCK", 0x46 => "DIK_SCROLL",
|
||||
0x47 => "DIK_NUMPAD7", 0x48 => "DIK_NUMPAD8", 0x49 => "DIK_NUMPAD9",
|
||||
0x4B => "DIK_NUMPAD4", 0x4C => "DIK_NUMPAD5", 0x4D => "DIK_NUMPAD6",
|
||||
0x4F => "DIK_NUMPAD1", 0x50 => "DIK_NUMPAD2", 0x51 => "DIK_NUMPAD3",
|
||||
0x52 => "DIK_NUMPAD0", 0x53 => "DIK_DECIMAL",
|
||||
0x57 => "DIK_F11", 0x58 => "DIK_F12", 0x64 => "DIK_F13",
|
||||
0x65 => "DIK_F14", 0x66 => "DIK_F15", 0x9C => "DIK_NUMPADENTER",
|
||||
0x9D => "DIK_RCONTROL", 0xB7 => "DIK_SYSRQ", 0xB8 => "DIK_RALT",
|
||||
0xC5 => "DIK_PAUSE", 0xC7 => "DIK_HOME",
|
||||
0xCB => "DIK_LEFT", 0xCF => "DIK_END", 0xD2 => "DIK_INSERT",
|
||||
0xD3 => "DIK_DELETE", 0xDB => "DIK_LWIN", 0xDC => "DIK_RWIN",
|
||||
0xDD => "DIK_APPS",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,13 @@ using System.Text.Json;
|
|||
namespace AcDream.UI.Abstractions.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP8: persisted bindings for DAT ActionMap rows that
|
||||
/// <c>RetailActionIdentityTable</c> has no <see cref="InputAction"/> for —
|
||||
/// mostly Emotes and CharacterSettings hotkeys (see that table's class doc for
|
||||
/// the full accounting). These rows still render, bind, conflict-check, and
|
||||
/// persist on the Configure Keyboard screen exactly like a mapped row; they
|
||||
/// just have no live gameplay consumer to dispatch through yet, so they live in
|
||||
/// their own small store rather than <see cref="KeyBindings"/>'s
|
||||
/// <see cref="InputAction"/>-keyed schema. Sibling file next to
|
||||
/// <c>keybinds.json</c> (D4 — no <c>.keymap</c> file interchange).
|
||||
/// Forward-compatible persisted bindings for an ActionMap row introduced by a
|
||||
/// future DAT revision. Campaign KB maps every one of the 306 rows in the
|
||||
/// supported Sept-2013 EoR DAT, so this sibling file has no production entries
|
||||
/// there; it only keeps an unknown future row visible and round-trippable
|
||||
/// instead of crashing an older client. The compatibility sibling file stays
|
||||
/// beside <c>keybinds.json</c>; installed-retail rows use the canonical
|
||||
/// <c>*.keymap</c> profile instead.
|
||||
/// </summary>
|
||||
public sealed class RetailUnmappedKeyBindings
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ public sealed class ChatVM : IDisposable, IChatCommandFeedback
|
|||
public string? LastOutgoingTellTarget =>
|
||||
_commandTargets.LastOutgoingTellTarget;
|
||||
|
||||
public string? LastMonarchSender =>
|
||||
_commandTargets.LastMonarchSender;
|
||||
|
||||
public string? LastPatronSender =>
|
||||
_commandTargets.LastPatronSender;
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback exposing the live framerate. Wired by
|
||||
/// <c>GameWindow</c> at construction so the client-side
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue