refactor(input): own gameplay action routing

Move the sole semantic action-priority graph, combat and diagnostic commands, and retained-root item-drop lifetime behind focused typed owners. Preserve retail toggle behavior, explicit auto-wield cancellation, shutdown quiescence, and symmetric callback cleanup.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 12:43:05 +02:00
parent 8b8afeefa3
commit 4eae9b4f5a
25 changed files with 2608 additions and 418 deletions

View file

@ -36,6 +36,14 @@ public enum CombatAttackAction
High,
}
/// <summary>
/// Retail's default-mode lookup returns the selected mode and, on the one
/// rejected held-item branch, the object used to format its user notice.
/// </summary>
public readonly record struct DefaultCombatModeDecision(
CombatMode Mode,
ClientObject? IncompatibleHeldItem);
/// <summary>
/// Retail input-facing combat decisions. The heavyweight parts of the combat
/// system remain server authoritative; this helper only maps UI intent to the
@ -92,6 +100,14 @@ public static class CombatInputPlanner
/// object intersecting the requested location mask.
/// </summary>
public static CombatMode GetDefaultCombatMode(
IReadOnlyList<ClientObject> orderedPlayerContents) =>
GetDefaultCombatModeDecision(orderedPlayerContents).Mode;
/// <summary>
/// Complete result of retail <c>GetDefaultCombatMode</c>, including the
/// incompatible Held object used by its exact failure notice.
/// </summary>
public static DefaultCombatModeDecision GetDefaultCombatModeDecision(
IReadOnlyList<ClientObject> orderedPlayerContents)
{
ArgumentNullException.ThrowIfNull(orderedPlayerContents);
@ -102,20 +118,22 @@ public static class CombatInputPlanner
{
// Retail COMBAT_USE_MISSILE = 2. Every other combat-use value in
// this primary weapon slot selects melee.
return weapon.CombatUse == 2
? CombatMode.Missile
: CombatMode.Melee;
return new(
weapon.CombatUse == 2
? CombatMode.Missile
: CombatMode.Melee,
IncompatibleHeldItem: null);
}
ClientObject? held = GetObjectAtLocation(
orderedPlayerContents, EquipMask.Held);
if (held is null)
return CombatMode.Melee;
return new(CombatMode.Melee, IncompatibleHeldItem: null);
// The decomp's byte-1 sign test is ITEM_TYPE bit 15 (Caster).
return (held.Type & ItemType.Caster) != 0
? CombatMode.Magic
: CombatMode.NonCombat;
? new(CombatMode.Magic, IncompatibleHeldItem: null)
: new(CombatMode.NonCombat, held);
}
private static ClientObject? GetObjectAtLocation(
@ -136,12 +154,13 @@ public static class CombatInputPlanner
CombatMode currentMode,
CombatMode defaultCombatMode = CombatMode.Melee)
{
if ((currentMode & CombatMode.CombatCombat) != 0)
// ClientCombatSystem::ToggleCombatMode @ 0x0056C8C0 compares the
// current value directly with NonCombat. It does not coerce a
// rejected/default NonCombat result to unarmed Melee.
if (currentMode != CombatMode.NonCombat)
return CombatMode.NonCombat;
return (defaultCombatMode & CombatMode.CombatCombat) != 0
? defaultCombatMode
: CombatMode.Melee;
return defaultCombatMode;
}
public static bool SupportsTargetedAttack(CombatMode mode) =>