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 System.Collections.Generic;
using System.Linq;
using AcDream.Core.Items;
using AcDream.Core.Spells;
using AcDream.Runtime.Gameplay;
using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
@ -158,25 +159,29 @@ public sealed class MagicCatalog
}
/// <summary>
/// App-layer owner for the live retail magic request path. The server owns
/// turning, motion, fizzle, mana/component consumption, effects, and results.
/// App content/policy projection for Runtime's live retail magic request
/// owner. The server owns turning, motion, fizzle, mana/component consumption,
/// effects, and results.
/// </summary>
public sealed class MagicRuntime
public sealed class MagicRuntime : IDisposable
{
private readonly SpellComponentRequirementService _requirements;
private IDisposable? _castOperationsBinding;
private MagicRuntime(
MagicCatalog catalog,
SpellCastingController casting,
SpellComponentRequirementService requirements)
RuntimeSpellCastState casting,
SpellComponentRequirementService requirements,
IDisposable castOperationsBinding)
{
Catalog = catalog;
Casting = casting;
_requirements = requirements;
_castOperationsBinding = castOperationsBinding;
}
public MagicCatalog Catalog { get; }
public SpellCastingController Casting { get; }
public RuntimeSpellCastState Casting { get; }
public IReadOnlyList<SpellExamineComponent> GetExamineComponents(uint spellId)
{
@ -200,11 +205,11 @@ public sealed class MagicRuntime
return result;
}
public static MagicRuntime Create(
internal static MagicRuntime Create(
MagicCatalog catalog,
Spellbook spellbook,
RuntimeSpellCastState casting,
RuntimeSpellCastOperationsSlot operationsSlot,
ClientObjectTable objects,
Func<uint?> selectedObject,
Func<uint> localPlayerId,
Func<string> accountName,
Action stopCompletely,
@ -215,39 +220,29 @@ public sealed class MagicRuntime
Func<bool> canSend)
{
ArgumentNullException.ThrowIfNull(catalog);
ArgumentNullException.ThrowIfNull(spellbook);
ArgumentNullException.ThrowIfNull(casting);
ArgumentNullException.ThrowIfNull(operationsSlot);
ArgumentNullException.ThrowIfNull(objects);
ArgumentNullException.ThrowIfNull(displayMessage);
SpellComponentRequirementService requirements = catalog.CreateRequirementService(
objects, localPlayerId, accountName);
bool TargetCompatible(uint target, SpellMetadata spell, bool showMessage)
{
if (objects.Get(target) is not { } targetObject)
return false;
SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate(
localPlayerId(), targetObject, spell);
if (showMessage && !result.Allowed && result.Message is { } message)
displayMessage(message);
return result.Allowed;
}
var casting = new SpellCastingController(
spellbook,
selectedObject,
var operations = new LiveSpellCastOperations(
requirements,
objects,
localPlayerId,
stopCompletely,
sendUntargeted,
sendTargeted,
displayMessage,
targetCompatible: (target, spell) => TargetCompatible(target, spell, true),
hasRequiredComponents: requirements.HasRequiredComponents,
incrementBusy: incrementBusy,
canSend: canSend,
targetCompatibleSilent: (target, spell) => TargetCompatible(target, spell, false));
return new MagicRuntime(catalog, casting, requirements);
incrementBusy,
canSend);
IDisposable binding = operationsSlot.BindOwned(operations);
return new MagicRuntime(catalog, casting, requirements, binding);
}
public void Reset() => Casting.Reset();
public void Dispose() =>
Interlocked.Exchange(ref _castOperationsBinding, null)?.Dispose();
}