refactor(runtime): own combat and magic intent

Move attack build/repeat state, combat-mode policy, authoritative auto-target transitions, and spell-cast intent beneath RuntimeActionState. Keep App as the input, world-query, DAT-policy, transport, and presentation adapter while preserving retail request and busy ordering. Add direct/graphical parity, reset, failure, and instance-isolation coverage.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-26 11:56:40 +02:00
parent 81b31857c6
commit 20df9d155d
49 changed files with 1949 additions and 634 deletions

View file

@ -3,6 +3,7 @@ using AcDream.App.Net;
using AcDream.App.UI;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
namespace AcDream.App.Combat;
@ -164,37 +165,96 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand
}
}
/// <summary>
/// Owns the input-facing combat-mode command. The choice itself remains the
/// retail port in <see cref="CombatInputPlanner"/>:
/// <c>ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310</c> and
/// <c>ClientCombatSystem::ToggleCombatMode @ 0x0056C8C0</c>. ACE remains the
/// authority; the local state update preserves the accepted immediate toolbar
/// response while the server confirmation travels through the normal route.
/// </summary>
internal sealed class LiveCombatModeCommandController : ILiveCombatModeCommand
internal sealed class RuntimeCombatModeOperationsSlot
: IRuntimeCombatModeOperations
{
private IRuntimeCombatModeOperations? _owner;
public IDisposable BindOwned(IRuntimeCombatModeOperations owner)
{
ArgumentNullException.ThrowIfNull(owner);
if (_owner is not null)
throw new InvalidOperationException(
"Runtime combat-mode operations are already bound.");
_owner = owner;
return new Binding(this, owner);
}
private void Unbind(IRuntimeCombatModeOperations expected)
{
if (ReferenceEquals(_owner, expected))
_owner = null;
}
public bool IsInWorld => _owner?.IsInWorld == true;
public IReadOnlyList<ClientObject> GetOrderedEquipment() =>
_owner?.GetOrderedEquipment() ?? [];
public void NotifyExplicitCombatModeRequest() =>
_owner?.NotifyExplicitCombatModeRequest();
public void SendChangeCombatMode(CombatMode mode) =>
_owner?.SendChangeCombatMode(mode);
private sealed class Binding : IDisposable
{
private RuntimeCombatModeOperationsSlot? _slot;
private readonly IRuntimeCombatModeOperations _expected;
public Binding(
RuntimeCombatModeOperationsSlot slot,
IRuntimeCombatModeOperations expected)
{
_slot = slot;
_expected = expected;
}
public void Dispose() =>
Interlocked.Exchange(ref _slot, null)?.Unbind(_expected);
}
}
internal sealed class LiveCombatModeOperations
: IRuntimeCombatModeOperations
{
private readonly ILiveCombatModeAuthority _authority;
private readonly ICombatEquipmentSource _equipment;
private readonly CombatState _combat;
private readonly IExplicitCombatModeIntentSink _itemIntent;
public LiveCombatModeOperations(
ILiveCombatModeAuthority authority,
ICombatEquipmentSource equipment,
IExplicitCombatModeIntentSink itemIntent)
{
_authority = authority ?? throw new ArgumentNullException(nameof(authority));
_equipment = equipment ?? throw new ArgumentNullException(nameof(equipment));
_itemIntent = itemIntent ?? throw new ArgumentNullException(nameof(itemIntent));
}
public bool IsInWorld => _authority.IsInWorld;
public IReadOnlyList<ClientObject> GetOrderedEquipment() =>
_equipment.GetOrderedEquipment();
public void NotifyExplicitCombatModeRequest() =>
_itemIntent.NotifyExplicitCombatModeRequest();
public void SendChangeCombatMode(CombatMode mode) =>
_authority.SendChangeCombatMode(mode);
}
/// <summary>
/// App presentation adapter for Runtime's retail combat-mode decision.
/// </summary>
internal sealed class RuntimeCombatModeCommandAdapter : ILiveCombatModeCommand
{
private readonly RuntimeCombatModeState _owner;
private readonly Action<string> _log;
private readonly Action<string>? _toast;
private readonly Action<string> _systemMessage;
public LiveCombatModeCommandController(
ILiveCombatModeAuthority authority,
ICombatEquipmentSource equipment,
CombatState combat,
IExplicitCombatModeIntentSink itemIntent,
public RuntimeCombatModeCommandAdapter(
RuntimeCombatModeState owner,
Action<string>? log = null,
Action<string>? toast = null,
Action<string>? systemMessage = null)
{
_authority = authority ?? throw new ArgumentNullException(nameof(authority));
_equipment = equipment ?? throw new ArgumentNullException(nameof(equipment));
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
_itemIntent = itemIntent ?? throw new ArgumentNullException(nameof(itemIntent));
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
_log = log ?? (_ => { });
_toast = toast;
_systemMessage = systemMessage ?? (_ => { });
@ -202,48 +262,19 @@ internal sealed class LiveCombatModeCommandController : ILiveCombatModeCommand
public void Toggle()
{
if (!_authority.IsInWorld)
RuntimeCombatModeRequestResult result = _owner.Toggle();
if (result.Status == RuntimeCombatModeRequestStatus.Inactive)
return;
// Every explicit user combat request supersedes an in-flight auto-wield
// settlement, including a request retail rejects before SetCombatMode.
_itemIntent.NotifyExplicitCombatModeRequest();
CombatMode currentMode = _combat.CurrentMode;
CombatMode nextMode;
if (currentMode != CombatMode.NonCombat)
if (result.Status == RuntimeCombatModeRequestStatus.Rejected)
{
// ToggleCombatMode @ 0x0056C8C0 immediately selects NonCombat and
// never queries default equipment on this branch.
nextMode = CombatMode.NonCombat;
}
else
{
DefaultCombatModeDecision defaultDecision =
CombatInputPlanner.GetDefaultCombatModeDecision(
_equipment.GetOrderedEquipment());
nextMode = CombatInputPlanner.ToggleMode(
currentMode,
defaultDecision.Mode);
// GetDefaultCombatMode prints this notice before SetCombatMode.
// SetCombatMode then sees NonCombat == NonCombat and does no work.
if (defaultDecision.IncompatibleHeldItem is { } held)
{
string notice =
$"You can't enter combat mode while wielding the {held.GetAppropriateName()}";
_log($"combat: {notice}");
_systemMessage(notice);
return;
}
string notice = result.Notice ?? string.Empty;
_log($"combat: {notice}");
_systemMessage(notice);
return;
}
// Preserve the accepted command order: explicit intent was published
// above, then send the request and publish local presentation.
_authority.SendChangeCombatMode(nextMode);
_combat.SetCombatMode(nextMode);
string message = $"Combat mode {nextMode}";
string message = $"Combat mode {result.Mode}";
_log($"combat: {message}");
_toast?.Invoke(message);
}