refactor(runtime): expose canonical gameplay state
Move character options and movement skills into the Runtime-owned character graph, expose borrowed inventory, character, and social views, and route retained UI state commands through generation-gated typed Runtime contracts. Preserve the existing synchronous wire path while deleting the App-owned option and skill mirrors and extending normalized parity checkpoints. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
9d0d9b07e0
commit
dcb61efb5a
34 changed files with 2076 additions and 103 deletions
|
|
@ -18,6 +18,7 @@ using AcDream.Core.Items;
|
|||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
|
|
@ -56,7 +57,6 @@ internal sealed record InteractionRetainedUiDependencies(
|
|||
ILocalPlayerIdentitySource PlayerIdentity,
|
||||
ILocalPlayerControllerSource PlayerController,
|
||||
ILocalPlayerModeSource PlayerMode,
|
||||
PlayerCharacterOptionsState CharacterOptions,
|
||||
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
|
||||
SelectionCameraFactory,
|
||||
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
|
||||
|
|
@ -118,6 +118,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
|
|||
private bool _deactivationStarted;
|
||||
|
||||
public DeferredLiveSessionUiAuthority Session { get; } = new();
|
||||
public DeferredGameRuntimeStateCommands GameRuntime { get; } = new();
|
||||
public DeferredSelectionUiAuthority Selection { get; } = new();
|
||||
public DeferredSelectionViewPlaneSource SelectionViewPlane { get; } = new();
|
||||
public DeferredRadarSnapshotSource Radar { get; } = new();
|
||||
|
|
@ -176,6 +177,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
|
|||
Radar.Deactivate();
|
||||
SelectionViewPlane.Deactivate();
|
||||
Selection.Deactivate();
|
||||
GameRuntime.Deactivate();
|
||||
Session.Deactivate();
|
||||
InventoryContainer.Deactivate();
|
||||
for (int i = _lateOwnerBindings.Count - 1; i >= 0; i--)
|
||||
|
|
@ -294,7 +296,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
sendGive: (target, item, amount) =>
|
||||
session.CurrentSession?.SendGiveObject(target, item, amount),
|
||||
dragOnPlayerOpensSecureTrade: () =>
|
||||
d.CharacterOptions.DragItemOnPlayerOpensSecureTrade,
|
||||
d.Character.Options.DragItemOnPlayerOpensSecureTrade,
|
||||
toast: d.Toast,
|
||||
readyForInventoryRequest: () => session.IsInWorld,
|
||||
playerOnGround: () =>
|
||||
|
|
@ -375,15 +377,27 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
activeToonName: () => d.Settings.ActiveToonKey,
|
||||
fallbackSheet: Studio.SampleData.SampleCharacter,
|
||||
canSendRaise: () => late.Session.IsInWorld,
|
||||
canSendRaise: () => late.GameRuntime.IsInWorld,
|
||||
sendRaiseAttribute: (statId, cost) =>
|
||||
late.Session.CurrentSession?.SendRaiseAttribute(statId, cost),
|
||||
late.GameRuntime.Advance(
|
||||
RuntimeAdvancementKind.Attribute,
|
||||
statId,
|
||||
cost),
|
||||
sendRaiseVital: (statId, cost) =>
|
||||
late.Session.CurrentSession?.SendRaiseVital(statId, cost),
|
||||
late.GameRuntime.Advance(
|
||||
RuntimeAdvancementKind.Vital,
|
||||
statId,
|
||||
cost),
|
||||
sendRaiseSkill: (statId, cost) =>
|
||||
late.Session.CurrentSession?.SendRaiseSkill(statId, cost),
|
||||
late.GameRuntime.Advance(
|
||||
RuntimeAdvancementKind.Skill,
|
||||
statId,
|
||||
cost),
|
||||
sendTrainSkill: (statId, credits) =>
|
||||
late.Session.CurrentSession?.SendTrainSkill(statId, credits));
|
||||
late.GameRuntime.Advance(
|
||||
RuntimeAdvancementKind.TrainSkill,
|
||||
statId,
|
||||
credits));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.CharacterSheetCreated);
|
||||
|
||||
MagicRuntime magic = MagicRuntime.Create(
|
||||
|
|
@ -543,16 +557,13 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
|
||||
guid => late.Session.TryUseItem(guid, d.Log),
|
||||
(tab, position, spellId) =>
|
||||
late.Session.CurrentSession?.SendAddSpellFavorite(
|
||||
spellId,
|
||||
position,
|
||||
tab),
|
||||
late.GameRuntime.AddFavorite(tab, position, spellId),
|
||||
(tab, spellId) =>
|
||||
late.Session.CurrentSession?.SendRemoveSpellFavorite(spellId, tab),
|
||||
filters => late.Session.CurrentSession?.SendSpellbookFilter(filters),
|
||||
spellId => late.Session.CurrentSession?.SendRemoveSpell(spellId),
|
||||
late.GameRuntime.RemoveFavorite(tab, spellId),
|
||||
filters => late.GameRuntime.SetSpellbookFilter(filters),
|
||||
spellId => late.GameRuntime.ForgetSpell(spellId),
|
||||
(componentId, amount) =>
|
||||
late.Session.CurrentSession?.SendSetDesiredComponentLevel(
|
||||
late.GameRuntime.SetDesiredComponent(
|
||||
componentId,
|
||||
amount),
|
||||
d.ClientTime),
|
||||
|
|
@ -589,8 +600,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.Inventory.ItemMana,
|
||||
d.CombatModeCommands.Toggle,
|
||||
itemInteraction,
|
||||
entry => late.Session.CurrentSession?.SendAddShortcut(entry),
|
||||
index => late.Session.CurrentSession?.SendRemoveShortcut(index),
|
||||
entry => late.GameRuntime.AddShortcut(entry),
|
||||
index => late.GameRuntime.RemoveShortcut(index),
|
||||
d.Selection,
|
||||
handler => d.Combat.HealthChanged += handler,
|
||||
handler => d.Combat.HealthChanged -= handler,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,186 @@ using AcDream.App.UI.Testing;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.UI.Abstractions;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Early retained-UI projection over Slice J's later current-runtime seam.
|
||||
/// Every call captures the view and command owner together and supplies that
|
||||
/// exact generation, so a displaced session can never receive the command.
|
||||
/// </summary>
|
||||
internal sealed class DeferredGameRuntimeStateCommands
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private IGameRuntimeView? _view;
|
||||
private IGameRuntimeCommands? _commands;
|
||||
private bool _deactivated;
|
||||
|
||||
public bool IsInWorld
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
return !_deactivated
|
||||
&& _view?.Lifecycle.State == RuntimeLifecycleState.InWorld;
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Bind(
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(view);
|
||||
ArgumentNullException.ThrowIfNull(commands);
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||
if (_view is not null || _commands is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The retained-UI game-runtime command seam is already bound.");
|
||||
}
|
||||
_view = view;
|
||||
_commands = commands;
|
||||
}
|
||||
return new ExpectedRuntimeBinding(this, view, commands);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult AddShortcut(ShortcutEntry entry) =>
|
||||
Invoke((commands, generation) => commands.InventoryState.AddShortcut(
|
||||
generation,
|
||||
new RuntimeShortcutCommand(
|
||||
entry.Index,
|
||||
entry.ObjectId,
|
||||
entry.SpellId)));
|
||||
|
||||
public RuntimeCommandResult RemoveShortcut(uint index)
|
||||
{
|
||||
if (index > int.MaxValue)
|
||||
return CurrentResult(RuntimeCommandStatus.Rejected);
|
||||
return Invoke((commands, generation) =>
|
||||
commands.InventoryState.RemoveShortcut(
|
||||
generation,
|
||||
(int)index));
|
||||
}
|
||||
|
||||
public RuntimeCommandResult AddFavorite(
|
||||
int tab,
|
||||
int position,
|
||||
uint spellId) =>
|
||||
Invoke((commands, generation) => commands.Spellbook.AddFavorite(
|
||||
generation,
|
||||
tab,
|
||||
position,
|
||||
spellId));
|
||||
|
||||
public RuntimeCommandResult RemoveFavorite(int tab, uint spellId) =>
|
||||
Invoke((commands, generation) => commands.Spellbook.RemoveFavorite(
|
||||
generation,
|
||||
tab,
|
||||
spellId));
|
||||
|
||||
public RuntimeCommandResult SetSpellbookFilter(uint filters) =>
|
||||
Invoke((commands, generation) => commands.Spellbook.SetFilter(
|
||||
generation,
|
||||
filters));
|
||||
|
||||
public RuntimeCommandResult ForgetSpell(uint spellId) =>
|
||||
Invoke((commands, generation) => commands.Spellbook.ForgetSpell(
|
||||
generation,
|
||||
spellId));
|
||||
|
||||
public RuntimeCommandResult SetDesiredComponent(
|
||||
uint componentId,
|
||||
uint amount) =>
|
||||
Invoke((commands, generation) =>
|
||||
commands.Spellbook.SetDesiredComponent(
|
||||
generation,
|
||||
componentId,
|
||||
amount));
|
||||
|
||||
public RuntimeCommandResult Advance(
|
||||
RuntimeAdvancementKind kind,
|
||||
uint statId,
|
||||
ulong cost) =>
|
||||
Invoke((commands, generation) => commands.Character.Advance(
|
||||
generation,
|
||||
new RuntimeAdvancementCommand(kind, statId, cost)));
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_deactivated = true;
|
||||
_view = null;
|
||||
_commands = null;
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeCommandResult Invoke(
|
||||
Func<
|
||||
IGameRuntimeCommands,
|
||||
RuntimeGenerationToken,
|
||||
RuntimeCommandResult> invoke)
|
||||
{
|
||||
IGameRuntimeCommands commands;
|
||||
RuntimeGenerationToken generation;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_deactivated || _view is null || _commands is null)
|
||||
{
|
||||
return new RuntimeCommandResult(
|
||||
RuntimeCommandStatus.Inactive,
|
||||
_view?.Generation ?? default);
|
||||
}
|
||||
commands = _commands;
|
||||
generation = _view.Generation;
|
||||
}
|
||||
return invoke(commands, generation);
|
||||
}
|
||||
|
||||
private RuntimeCommandResult CurrentResult(RuntimeCommandStatus status)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return new RuntimeCommandResult(
|
||||
status,
|
||||
_view?.Generation ?? default);
|
||||
}
|
||||
}
|
||||
|
||||
private void Release(
|
||||
IGameRuntimeView expectedView,
|
||||
IGameRuntimeCommands expectedCommands)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!ReferenceEquals(_view, expectedView)
|
||||
|| !ReferenceEquals(_commands, expectedCommands))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_view = null;
|
||||
_commands = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ExpectedRuntimeBinding(
|
||||
DeferredGameRuntimeStateCommands owner,
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands)
|
||||
: IDisposable
|
||||
{
|
||||
private DeferredGameRuntimeStateCommands? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Release(view, commands);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Early UI view over the later live-session owner. A binding lease clears only
|
||||
/// the exact owner it installed, so rollback cannot withdraw a replacement.
|
||||
|
|
@ -394,19 +569,6 @@ internal sealed class DeferredInventoryContainerSource
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class PlayerCharacterOptionsState
|
||||
{
|
||||
public PlayerDescriptionParser.CharacterOptions1 Options { get; set; } =
|
||||
PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
|
||||
public bool DragItemOnPlayerOpensSecureTrade =>
|
||||
(Options
|
||||
& PlayerDescriptionParser.CharacterOptions1
|
||||
.DragItemOnPlayerOpensSecureTrade) != 0;
|
||||
|
||||
public void Reset() => Options = PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
|
||||
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.
|
||||
|
|
|
|||
|
|
@ -68,10 +68,8 @@ internal sealed record SessionPlayerDependencies(
|
|||
LiveWorldOriginState WorldOrigin,
|
||||
WorldRenderRangeState RenderRange,
|
||||
LocalPlayerShadowState PlayerShadow,
|
||||
LocalPlayerSkillState PlayerSkills,
|
||||
ViewportAspectState ViewportAspect,
|
||||
PlayerApproachCompletionState PlayerApproachCompletions,
|
||||
PlayerCharacterOptionsState CharacterOptions,
|
||||
RuntimeInventoryState Inventory,
|
||||
PointerPositionState PointerPosition,
|
||||
DispatcherMovementInputSource MovementInput,
|
||||
|
|
@ -710,7 +708,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
gameplayInput,
|
||||
liveSessionSource,
|
||||
d.MovementDiagnostics,
|
||||
d.PlayerSkills,
|
||||
d.Character.MovementSkills,
|
||||
d.ViewportAspect);
|
||||
if (d.SettingsDevTools.DevTools is { } devTools)
|
||||
{
|
||||
|
|
@ -796,9 +794,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
new LiveSessionPlayerRuntime(
|
||||
d.PlayerIdentity,
|
||||
d.PlayerController,
|
||||
d.PlayerSkills,
|
||||
d.WorldOrigin,
|
||||
d.CharacterOptions),
|
||||
d.WorldOrigin),
|
||||
new LiveSessionDomainRuntime(
|
||||
d.EntityObjects,
|
||||
d.Character,
|
||||
|
|
@ -876,6 +872,8 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.PlayerIdentity,
|
||||
live.LiveEntities,
|
||||
d.EntityObjects,
|
||||
d.Inventory,
|
||||
d.Character,
|
||||
d.Communication,
|
||||
d.PlayerController,
|
||||
worldReveal,
|
||||
|
|
@ -885,6 +883,9 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
gameplayInput,
|
||||
combatCommand);
|
||||
bindings.Adopt("current game runtime adapter", gameRuntime);
|
||||
bindings.Adopt(
|
||||
"retained-UI game runtime commands",
|
||||
interaction.LateBindings.GameRuntime.Bind(gameRuntime, gameRuntime));
|
||||
|
||||
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
|
||||
new RuntimeNearbyWorldDiagnosticSource(
|
||||
|
|
|
|||
|
|
@ -140,38 +140,21 @@ internal sealed class LocalPlayerModeState : ILocalPlayerModeSource
|
|||
/// delivery and player-mode construction share this owner so rebuilding the
|
||||
/// local physics controller cannot fall back to stale defaults.
|
||||
/// </summary>
|
||||
internal sealed class LocalPlayerSkillState
|
||||
internal static class LocalPlayerSkillProjection
|
||||
{
|
||||
public int RunSkill { get; private set; } = -1;
|
||||
public int JumpSkill { get; private set; } = -1;
|
||||
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
|
||||
|
||||
public void Update(
|
||||
int runSkill,
|
||||
int jumpSkill,
|
||||
public static bool ApplyTo(
|
||||
AcDream.Runtime.Gameplay.RuntimeMovementSkillState skills,
|
||||
PlayerMovementController? controller)
|
||||
{
|
||||
if (runSkill >= 0)
|
||||
RunSkill = runSkill;
|
||||
if (jumpSkill >= 0)
|
||||
JumpSkill = jumpSkill;
|
||||
ApplyTo(controller);
|
||||
}
|
||||
|
||||
public bool ApplyTo(PlayerMovementController? controller)
|
||||
{
|
||||
if (controller is null || !IsComplete)
|
||||
ArgumentNullException.ThrowIfNull(skills);
|
||||
AcDream.Runtime.Gameplay.RuntimeMovementSkillSnapshot snapshot =
|
||||
skills.Snapshot;
|
||||
if (controller is null || !snapshot.IsComplete)
|
||||
return false;
|
||||
|
||||
controller.SetCharacterSkills(RunSkill, JumpSkill);
|
||||
controller.SetCharacterSkills(snapshot.RunSkill, snapshot.JumpSkill);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
RunSkill = -1;
|
||||
JumpSkill = -1;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IViewportAspectSource
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using AcDream.Content;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ internal sealed class PlayerModeController :
|
|||
private readonly ILocalPlayerTeleportInputLifetime _input;
|
||||
private readonly ILiveInWorldSource _session;
|
||||
private readonly MovementTruthDiagnosticController _movementDiagnostics;
|
||||
private readonly LocalPlayerSkillState _skills;
|
||||
private readonly RuntimeMovementSkillState _skills;
|
||||
private readonly IViewportAspectSource _viewport;
|
||||
private PlayerModeAutoEntry? _autoEntry;
|
||||
private IPlayerApproachCompletionSink? _approachLifetime;
|
||||
|
|
@ -67,7 +68,7 @@ internal sealed class PlayerModeController :
|
|||
ILocalPlayerTeleportInputLifetime input,
|
||||
ILiveInWorldSource session,
|
||||
MovementTruthDiagnosticController movementDiagnostics,
|
||||
LocalPlayerSkillState skills,
|
||||
RuntimeMovementSkillState skills,
|
||||
IViewportAspectSource viewport)
|
||||
{
|
||||
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||
|
|
@ -359,7 +360,7 @@ internal sealed class PlayerModeController :
|
|||
exactMovement.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
};
|
||||
|
||||
if (_skills.ApplyTo(controller))
|
||||
if (LocalPlayerSkillProjection.ApplyTo(_skills, controller))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: {loggingTag} — applied server skills "
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
|
@ -15,8 +16,64 @@ internal sealed record LiveSessionCommandBindings(
|
|||
Action<string, string> SendTell,
|
||||
Action<uint, string> SendChannel,
|
||||
Action<uint, uint, uint, uint, string, uint> SendTurbineChat,
|
||||
Action<ShortcutEntry> AddShortcut,
|
||||
Action<uint> RemoveShortcut,
|
||||
Action<uint, int, int> AddFavorite,
|
||||
Action<uint, int> RemoveFavorite,
|
||||
Action<uint> SetSpellbookFilter,
|
||||
Action<uint> ForgetSpell,
|
||||
Action<uint, uint> SetDesiredComponent,
|
||||
Action ClearDesiredComponents,
|
||||
Action<uint, ulong> RaiseAttribute,
|
||||
Action<uint, ulong> RaiseVital,
|
||||
Action<uint, ulong> RaiseSkill,
|
||||
Action<uint, uint> TrainSkill,
|
||||
Action<uint> SetCharacterOptions,
|
||||
Action<string> AddFriend,
|
||||
Action<uint> RemoveFriend,
|
||||
Action ClearFriends,
|
||||
Action RequestLegacyFriends,
|
||||
Action<bool, uint, string, uint> ModifyCharacterSquelch,
|
||||
Action<bool, string> ModifyAccountSquelch,
|
||||
Action<bool, uint> ModifyGlobalSquelch,
|
||||
Action<string>? Log = null);
|
||||
|
||||
internal readonly record struct AddShortcutRuntimeCmd(ShortcutEntry Entry);
|
||||
internal readonly record struct RemoveShortcutRuntimeCmd(uint Index);
|
||||
internal readonly record struct AddFavoriteRuntimeCmd(
|
||||
uint SpellId,
|
||||
int Position,
|
||||
int TabIndex);
|
||||
internal readonly record struct RemoveFavoriteRuntimeCmd(
|
||||
uint SpellId,
|
||||
int TabIndex);
|
||||
internal readonly record struct SetSpellbookFilterRuntimeCmd(uint Filters);
|
||||
internal readonly record struct ForgetSpellRuntimeCmd(uint SpellId);
|
||||
internal readonly record struct SetDesiredComponentRuntimeCmd(
|
||||
uint ComponentId,
|
||||
uint Amount);
|
||||
internal readonly record struct ClearDesiredComponentsRuntimeCmd;
|
||||
internal readonly record struct RaiseAttributeRuntimeCmd(uint StatId, ulong Cost);
|
||||
internal readonly record struct RaiseVitalRuntimeCmd(uint StatId, ulong Cost);
|
||||
internal readonly record struct RaiseSkillRuntimeCmd(uint StatId, ulong Cost);
|
||||
internal readonly record struct TrainSkillRuntimeCmd(uint StatId, uint Cost);
|
||||
internal readonly record struct SetCharacterOptionsRuntimeCmd(uint Options);
|
||||
internal readonly record struct AddFriendRuntimeCmd(string Name);
|
||||
internal readonly record struct RemoveFriendRuntimeCmd(uint CharacterId);
|
||||
internal readonly record struct ClearFriendsRuntimeCmd;
|
||||
internal readonly record struct RequestLegacyFriendsRuntimeCmd;
|
||||
internal readonly record struct ModifyCharacterSquelchRuntimeCmd(
|
||||
bool Add,
|
||||
uint CharacterId,
|
||||
string Name,
|
||||
uint MessageType);
|
||||
internal readonly record struct ModifyAccountSquelchRuntimeCmd(
|
||||
bool Add,
|
||||
string Name);
|
||||
internal readonly record struct ModifyGlobalSquelchRuntimeCmd(
|
||||
bool Add,
|
||||
uint MessageType);
|
||||
|
||||
/// <summary>
|
||||
/// Owns the command surface for one exact live-session generation. The router
|
||||
/// itself is the published bus, so a retained reference becomes inert before
|
||||
|
|
@ -52,6 +109,68 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting
|
|||
SendIfActive(() => bindings.SendTalk(command.Text));
|
||||
});
|
||||
commands.Register<SendChatCmd>(command => RouteChat(bindings, command));
|
||||
commands.Register<AddShortcutRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.AddShortcut(command.Entry)));
|
||||
commands.Register<RemoveShortcutRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.RemoveShortcut(command.Index)));
|
||||
commands.Register<AddFavoriteRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.AddFavorite(
|
||||
command.SpellId,
|
||||
command.Position,
|
||||
command.TabIndex)));
|
||||
commands.Register<RemoveFavoriteRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.RemoveFavorite(
|
||||
command.SpellId,
|
||||
command.TabIndex)));
|
||||
commands.Register<SetSpellbookFilterRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.SetSpellbookFilter(command.Filters)));
|
||||
commands.Register<ForgetSpellRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.ForgetSpell(command.SpellId)));
|
||||
commands.Register<SetDesiredComponentRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.SetDesiredComponent(
|
||||
command.ComponentId,
|
||||
command.Amount)));
|
||||
commands.Register<ClearDesiredComponentsRuntimeCmd>(
|
||||
_ => SendIfActive(bindings.ClearDesiredComponents));
|
||||
commands.Register<RaiseAttributeRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.RaiseAttribute(command.StatId, command.Cost)));
|
||||
commands.Register<RaiseVitalRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.RaiseVital(command.StatId, command.Cost)));
|
||||
commands.Register<RaiseSkillRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.RaiseSkill(command.StatId, command.Cost)));
|
||||
commands.Register<TrainSkillRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.TrainSkill(command.StatId, command.Cost)));
|
||||
commands.Register<SetCharacterOptionsRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.SetCharacterOptions(command.Options)));
|
||||
commands.Register<AddFriendRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.AddFriend(command.Name)));
|
||||
commands.Register<RemoveFriendRuntimeCmd>(
|
||||
command => SendIfActive(() =>
|
||||
bindings.RemoveFriend(command.CharacterId)));
|
||||
commands.Register<ClearFriendsRuntimeCmd>(
|
||||
_ => SendIfActive(bindings.ClearFriends));
|
||||
commands.Register<RequestLegacyFriendsRuntimeCmd>(
|
||||
_ => SendIfActive(bindings.RequestLegacyFriends));
|
||||
commands.Register<ModifyCharacterSquelchRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.ModifyCharacterSquelch(
|
||||
command.Add,
|
||||
command.CharacterId,
|
||||
command.Name,
|
||||
command.MessageType)));
|
||||
commands.Register<ModifyAccountSquelchRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.ModifyAccountSquelch(
|
||||
command.Add,
|
||||
command.Name)));
|
||||
commands.Register<ModifyGlobalSquelchRuntimeCmd>(
|
||||
command => SendIfActive(() => bindings.ModifyGlobalSquelch(
|
||||
command.Add,
|
||||
command.MessageType)));
|
||||
_commands = commands;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,7 @@ namespace AcDream.App.Net;
|
|||
internal sealed record LiveSessionPlayerRuntime(
|
||||
LocalPlayerIdentityState Identity,
|
||||
LocalPlayerControllerSlot Controller,
|
||||
LocalPlayerSkillState Skills,
|
||||
LiveWorldOriginState WorldOrigin,
|
||||
PlayerCharacterOptionsState CharacterOptions);
|
||||
LiveWorldOriginState WorldOrigin);
|
||||
|
||||
internal sealed record LiveSessionDomainRuntime(
|
||||
RuntimeEntityObjectLifetime EntityObjects,
|
||||
|
|
@ -215,8 +213,8 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_domain.Communication.ResetChatIdentity();
|
||||
EntityVanishProbe.PlayerGuid = 0u;
|
||||
_interaction.Settings.ResetActiveCharacterKey();
|
||||
_player.CharacterOptions.Reset();
|
||||
_player.Skills.ResetSession();
|
||||
_domain.Character.Options.ResetSession();
|
||||
_domain.Character.MovementSkills.ResetSession();
|
||||
_world.NetworkUpdates.ResetSessionState();
|
||||
_world.Hydration.ResetSessionState();
|
||||
_domain.Inventory.ResetPlayerSnapshots();
|
||||
|
|
@ -282,27 +280,22 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
||||
{
|
||||
_player.Skills.Update(
|
||||
runSkill,
|
||||
jumpSkill,
|
||||
_player.Controller.Controller);
|
||||
if (_player.Skills.IsComplete
|
||||
&& _player.Controller.Controller is not null)
|
||||
if (LocalPlayerSkillProjection.ApplyTo(
|
||||
_domain.Character.MovementSkills,
|
||||
_player.Controller.Controller))
|
||||
{
|
||||
RuntimeMovementSkillSnapshot snapshot =
|
||||
_domain.Character.MovementSkills.Snapshot;
|
||||
_log(
|
||||
$"player: applied server skills " +
|
||||
$"run={_player.Skills.RunSkill} " +
|
||||
$"jump={_player.Skills.JumpSkill}");
|
||||
$"run={snapshot.RunSkill} " +
|
||||
$"jump={snapshot.JumpSkill}");
|
||||
}
|
||||
},
|
||||
OnConfirmationRequest: request =>
|
||||
_ui.RetailUi?.HandleConfirmationRequest(request),
|
||||
OnConfirmationDone: done =>
|
||||
_ui.RetailUi?.HandleConfirmationDone(done),
|
||||
OnCharacterOptions: (options1, _) =>
|
||||
_player.CharacterOptions.Options =
|
||||
(Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
||||
options1,
|
||||
ClientTime: ClientTimerNow);
|
||||
}
|
||||
|
||||
|
|
@ -391,6 +384,26 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
senderGuid,
|
||||
text,
|
||||
cookie),
|
||||
AddShortcut: session.SendAddShortcut,
|
||||
RemoveShortcut: session.SendRemoveShortcut,
|
||||
AddFavorite: session.SendAddSpellFavorite,
|
||||
RemoveFavorite: session.SendRemoveSpellFavorite,
|
||||
SetSpellbookFilter: session.SendSpellbookFilter,
|
||||
ForgetSpell: session.SendRemoveSpell,
|
||||
SetDesiredComponent: session.SendSetDesiredComponentLevel,
|
||||
ClearDesiredComponents: session.SendClearDesiredComponents,
|
||||
RaiseAttribute: session.SendRaiseAttribute,
|
||||
RaiseVital: session.SendRaiseVital,
|
||||
RaiseSkill: session.SendRaiseSkill,
|
||||
TrainSkill: session.SendTrainSkill,
|
||||
SetCharacterOptions: session.SendSetCharacterOptions,
|
||||
AddFriend: session.SendAddFriend,
|
||||
RemoveFriend: session.SendRemoveFriend,
|
||||
ClearFriends: session.SendClearFriends,
|
||||
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
|
||||
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
|
||||
ModifyAccountSquelch: session.SendModifyAccountSquelch,
|
||||
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
|
||||
Log: _log);
|
||||
|
||||
private static double ClientTimerNow() =>
|
||||
|
|
|
|||
|
|
@ -440,12 +440,8 @@ public sealed class GameWindow :
|
|||
get => _localPlayerIdentity.ServerGuid;
|
||||
set => _localPlayerIdentity.ServerGuid = value;
|
||||
}
|
||||
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
|
||||
// replaces this before any in-world drag can normally occur.
|
||||
private readonly PlayerCharacterOptionsState _characterOptions = new();
|
||||
private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new();
|
||||
|
||||
private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = new();
|
||||
private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new();
|
||||
private readonly FramebufferResizeController _framebufferResize;
|
||||
private AcDream.App.Input.PlayerModeController? _playerModeController;
|
||||
|
|
@ -1269,7 +1265,6 @@ public sealed class GameWindow :
|
|||
_localPlayerIdentity,
|
||||
_playerControllerSlot,
|
||||
_localPlayerMode,
|
||||
_characterOptions,
|
||||
viewPlane => new SelectionCameraSource(
|
||||
hostInputCamera.CameraController,
|
||||
_window!,
|
||||
|
|
@ -1387,10 +1382,8 @@ public sealed class GameWindow :
|
|||
_liveWorldOrigin,
|
||||
_renderRange,
|
||||
_localPlayerShadow,
|
||||
_localPlayerSkills,
|
||||
_viewportAspect,
|
||||
_playerApproachCompletions,
|
||||
_characterOptions,
|
||||
_runtimeInventory,
|
||||
_pointerPosition,
|
||||
_movementInput,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
LocalPlayerIdentityState playerIdentity,
|
||||
LiveEntityRuntime entities,
|
||||
RuntimeEntityObjectLifetime entityObjects,
|
||||
RuntimeInventoryState inventory,
|
||||
RuntimeCharacterState character,
|
||||
RuntimeCommunicationState communication,
|
||||
ILocalPlayerControllerSource playerController,
|
||||
WorldRevealCoordinator worldReveal,
|
||||
|
|
@ -51,6 +53,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
playerIdentity,
|
||||
entities,
|
||||
entityObjects,
|
||||
inventory,
|
||||
character,
|
||||
communication,
|
||||
playerController,
|
||||
worldReveal,
|
||||
|
|
@ -79,6 +83,9 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
public IGameRuntimeClock Clock => _view.Clock;
|
||||
public IRuntimeEntityView Entities => _view.Entities;
|
||||
public IRuntimeInventoryView Inventory => _view.Inventory;
|
||||
public IRuntimeInventoryStateView InventoryState => _view.InventoryState;
|
||||
public IRuntimeCharacterView Character => _view.Character;
|
||||
public IRuntimeSocialView Social => _view.Social;
|
||||
public IRuntimeChatView Chat => _view.Chat;
|
||||
public IRuntimeMovementView Movement => _view.Movement;
|
||||
public IRuntimePortalView Portal => _view.Portal;
|
||||
|
|
@ -92,6 +99,15 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
IRuntimeChatCommands IGameRuntimeCommands.Chat => _commands;
|
||||
public IRuntimePortalCommands PortalCommands => _commands;
|
||||
IRuntimePortalCommands IGameRuntimeCommands.Portal => _commands;
|
||||
public IRuntimeInventoryStateCommands InventoryCommands => _commands;
|
||||
IRuntimeInventoryStateCommands IGameRuntimeCommands.InventoryState =>
|
||||
_commands;
|
||||
public IRuntimeSpellbookCommands SpellbookCommands => _commands;
|
||||
IRuntimeSpellbookCommands IGameRuntimeCommands.Spellbook => _commands;
|
||||
public IRuntimeCharacterCommands CharacterCommands => _commands;
|
||||
IRuntimeCharacterCommands IGameRuntimeCommands.Character => _commands;
|
||||
public IRuntimeSocialCommands SocialCommands => _commands;
|
||||
IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands;
|
||||
|
||||
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||
_view.CaptureCheckpoint();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Input;
|
|||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
|
@ -20,7 +21,11 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
IRuntimeCombatCommands,
|
||||
IRuntimeMovementCommands,
|
||||
IRuntimeChatCommands,
|
||||
IRuntimePortalCommands
|
||||
IRuntimePortalCommands,
|
||||
IRuntimeInventoryStateCommands,
|
||||
IRuntimeSpellbookCommands,
|
||||
IRuntimeCharacterCommands,
|
||||
IRuntimeSocialCommands
|
||||
{
|
||||
private readonly LiveSessionController _session;
|
||||
private readonly LiveSessionHost _sessionHost;
|
||||
|
|
@ -276,6 +281,340 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
return Result(RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult AddShortcut(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeShortcutCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (command.Index < 0
|
||||
|| (command.ObjectId == 0u && command.SpellId == 0u))
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 0,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
command.ObjectId);
|
||||
}
|
||||
|
||||
_commands.Publish(new AddShortcutRuntimeCmd(new ShortcutEntry(
|
||||
command.Index,
|
||||
command.ObjectId,
|
||||
command.SpellId)));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 0,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
command.ObjectId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult RemoveShortcut(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int index)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (index < 0)
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 1,
|
||||
RuntimeCommandStatus.Rejected);
|
||||
}
|
||||
|
||||
_commands.Publish(new RemoveShortcutRuntimeCmd((uint)index));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 1,
|
||||
RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult AddFavorite(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int tabIndex,
|
||||
int position,
|
||||
uint spellId)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if ((uint)tabIndex >= 8u
|
||||
|| position < 0
|
||||
|| spellId == 0u)
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 0,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
spellId);
|
||||
}
|
||||
|
||||
_commands.Publish(new AddFavoriteRuntimeCmd(
|
||||
spellId,
|
||||
position,
|
||||
tabIndex));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 0,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
spellId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult RemoveFavorite(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int tabIndex,
|
||||
uint spellId)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if ((uint)tabIndex >= 8u || spellId == 0u)
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 1,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
spellId);
|
||||
}
|
||||
|
||||
_commands.Publish(new RemoveFavoriteRuntimeCmd(spellId, tabIndex));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 1,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
spellId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetFilter(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint filters)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
_commands.Publish(new SetSpellbookFilterRuntimeCmd(filters));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 2,
|
||||
RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult ForgetSpell(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint spellId)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (spellId == 0u)
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 3,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
spellId);
|
||||
}
|
||||
|
||||
_commands.Publish(new ForgetSpellRuntimeCmd(spellId));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 3,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
spellId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetDesiredComponent(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint componentId,
|
||||
uint amount)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (componentId == 0u)
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 4,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
componentId);
|
||||
}
|
||||
|
||||
_commands.Publish(new SetDesiredComponentRuntimeCmd(
|
||||
componentId,
|
||||
amount));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 4,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
componentId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult ClearDesiredComponents(
|
||||
RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
_commands.Publish(new ClearDesiredComponentsRuntimeCmd());
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 5,
|
||||
RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Advance(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeAdvancementCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (command.StatId == 0u
|
||||
|| command.Cost == 0u)
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Character,
|
||||
(int)command.Kind,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
command.StatId);
|
||||
}
|
||||
|
||||
switch (command.Kind)
|
||||
{
|
||||
case RuntimeAdvancementKind.Attribute:
|
||||
_commands.Publish(new RaiseAttributeRuntimeCmd(
|
||||
command.StatId,
|
||||
command.Cost));
|
||||
break;
|
||||
case RuntimeAdvancementKind.Vital:
|
||||
_commands.Publish(new RaiseVitalRuntimeCmd(
|
||||
command.StatId,
|
||||
command.Cost));
|
||||
break;
|
||||
case RuntimeAdvancementKind.Skill:
|
||||
_commands.Publish(new RaiseSkillRuntimeCmd(
|
||||
command.StatId,
|
||||
command.Cost));
|
||||
break;
|
||||
case RuntimeAdvancementKind.TrainSkill
|
||||
when command.Cost <= uint.MaxValue:
|
||||
_commands.Publish(new TrainSkillRuntimeCmd(
|
||||
command.StatId,
|
||||
(uint)command.Cost));
|
||||
break;
|
||||
default:
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Character,
|
||||
(int)command.Kind,
|
||||
RuntimeCommandStatus.Rejected,
|
||||
command.StatId);
|
||||
}
|
||||
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Character,
|
||||
(int)command.Kind,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
command.StatId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult SetOptions1(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint options)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
_commands.Publish(new SetCharacterOptionsRuntimeCmd(options));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Character,
|
||||
operation: 4,
|
||||
RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeFriendCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
RuntimeCommandStatus status = RuntimeCommandStatus.Accepted;
|
||||
switch (command.Kind)
|
||||
{
|
||||
case RuntimeFriendCommandKind.Add
|
||||
when !string.IsNullOrWhiteSpace(command.Name):
|
||||
_commands.Publish(new AddFriendRuntimeCmd(command.Name));
|
||||
break;
|
||||
case RuntimeFriendCommandKind.Remove
|
||||
when command.CharacterId != 0u:
|
||||
_commands.Publish(new RemoveFriendRuntimeCmd(
|
||||
command.CharacterId));
|
||||
break;
|
||||
case RuntimeFriendCommandKind.Clear:
|
||||
_commands.Publish(new ClearFriendsRuntimeCmd());
|
||||
break;
|
||||
case RuntimeFriendCommandKind.RequestLegacyList:
|
||||
_commands.Publish(new RequestLegacyFriendsRuntimeCmd());
|
||||
break;
|
||||
default:
|
||||
status = RuntimeCommandStatus.Rejected;
|
||||
break;
|
||||
}
|
||||
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Social,
|
||||
(int)command.Kind,
|
||||
status,
|
||||
command.CharacterId,
|
||||
command.Name);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeSquelchCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
RuntimeCommandStatus status = RuntimeCommandStatus.Accepted;
|
||||
switch (command.Scope)
|
||||
{
|
||||
case RuntimeSquelchScope.Character
|
||||
when command.CharacterId != 0u
|
||||
&& !string.IsNullOrWhiteSpace(command.Name):
|
||||
_commands.Publish(new ModifyCharacterSquelchRuntimeCmd(
|
||||
command.Add,
|
||||
command.CharacterId,
|
||||
command.Name,
|
||||
command.MessageType));
|
||||
break;
|
||||
case RuntimeSquelchScope.Account
|
||||
when !string.IsNullOrWhiteSpace(command.Name):
|
||||
_commands.Publish(new ModifyAccountSquelchRuntimeCmd(
|
||||
command.Add,
|
||||
command.Name));
|
||||
break;
|
||||
case RuntimeSquelchScope.Global:
|
||||
_commands.Publish(new ModifyGlobalSquelchRuntimeCmd(
|
||||
command.Add,
|
||||
command.MessageType));
|
||||
break;
|
||||
default:
|
||||
status = RuntimeCommandStatus.Rejected;
|
||||
break;
|
||||
}
|
||||
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Social,
|
||||
operation: 4 + (int)command.Scope,
|
||||
status,
|
||||
command.CharacterId,
|
||||
command.Name);
|
||||
}
|
||||
|
||||
private RuntimeCommandStatus Validate(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
bool requireWorld)
|
||||
|
|
@ -294,6 +633,17 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
uint objectId = 0u) =>
|
||||
new(status, _view.Generation, objectId);
|
||||
|
||||
private RuntimeCommandResult EmitResult(
|
||||
RuntimeCommandDomain domain,
|
||||
int operation,
|
||||
RuntimeCommandStatus status,
|
||||
uint objectId = 0u,
|
||||
string? text = null)
|
||||
{
|
||||
_events.EmitCommand(domain, operation, status, objectId, text);
|
||||
return Result(status, objectId);
|
||||
}
|
||||
|
||||
private RuntimeSessionStartResult RejectedStart(RuntimeCommandStatus status) =>
|
||||
new(
|
||||
status == RuntimeCommandStatus.StaleGeneration
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
|||
private readonly IGameRuntimeClock _clock;
|
||||
private readonly IRuntimeEntityView _entityView;
|
||||
private readonly IRuntimeInventoryView _inventoryView;
|
||||
private readonly IRuntimeInventoryStateView _inventoryStateView;
|
||||
private readonly IRuntimeCharacterView _characterView;
|
||||
private readonly IRuntimeSocialView _socialView;
|
||||
private readonly IRuntimeChatView _chatView;
|
||||
private readonly MovementView _movementView;
|
||||
private readonly PortalView _portalView;
|
||||
|
|
@ -34,6 +37,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
|||
LocalPlayerIdentityState playerIdentity,
|
||||
LiveEntityRuntime entities,
|
||||
RuntimeEntityObjectLifetime entityObjects,
|
||||
RuntimeInventoryState inventory,
|
||||
RuntimeCharacterState character,
|
||||
RuntimeCommunicationState communication,
|
||||
ILocalPlayerControllerSource playerController,
|
||||
WorldRevealCoordinator worldReveal,
|
||||
|
|
@ -50,7 +55,12 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
|||
|
||||
_entityView = entityObjects.EntityView;
|
||||
_inventoryView = entityObjects.InventoryView;
|
||||
_inventoryStateView = (
|
||||
inventory ?? throw new ArgumentNullException(nameof(inventory))).View;
|
||||
_characterView = (
|
||||
character ?? throw new ArgumentNullException(nameof(character))).View;
|
||||
_chatView = communication.View;
|
||||
_socialView = communication.SocialView;
|
||||
_movementView = new MovementView(
|
||||
playerController
|
||||
?? throw new ArgumentNullException(nameof(playerController)),
|
||||
|
|
@ -90,6 +100,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
|||
public IGameRuntimeClock Clock => _clock;
|
||||
public IRuntimeEntityView Entities => _entityView;
|
||||
public IRuntimeInventoryView Inventory => _inventoryView;
|
||||
public IRuntimeInventoryStateView InventoryState => _inventoryStateView;
|
||||
public IRuntimeCharacterView Character => _characterView;
|
||||
public IRuntimeSocialView Social => _socialView;
|
||||
public IRuntimeChatView Chat => _chatView;
|
||||
public IRuntimeMovementView Movement => _movementView;
|
||||
public IRuntimePortalView Portal => _portalView;
|
||||
|
|
@ -103,6 +116,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
|||
_entities.MaterializedCount,
|
||||
_objects.ObjectCount,
|
||||
_objects.ContainerCount,
|
||||
_inventoryStateView.Snapshot,
|
||||
_characterView.Snapshot,
|
||||
_socialView.Snapshot,
|
||||
_chatView.Revision,
|
||||
_chatView.Count,
|
||||
_movementView.Snapshot,
|
||||
|
|
|
|||
|
|
@ -1747,6 +1747,16 @@ public sealed class WorldSession : IDisposable
|
|||
SendGameAction(ClientCommandRequests.BuildEmote(seq, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send retail SetCharacterOptions (0x01A1) for the first character
|
||||
/// option bitmap.
|
||||
/// </summary>
|
||||
public void SendSetCharacterOptions(uint options)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(SocialActions.BuildSetCharacterOptions(seq, options));
|
||||
}
|
||||
|
||||
public void SendAddFriend(string name)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace AcDream.Core.Items;
|
|||
public sealed class ItemManaState
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, float> _manaByGuid = new();
|
||||
private long _revision;
|
||||
|
||||
/// <summary>Fires for every valid or invalid query response.</summary>
|
||||
public event Action<uint /*guid*/, float /*fraction*/, bool /*valid*/>? ItemManaChanged;
|
||||
|
|
@ -20,6 +21,11 @@ public sealed class ItemManaState
|
|||
_manaByGuid.TryGetValue(guid, out float percent) ? percent : 0f;
|
||||
|
||||
public bool HasMana(uint guid) => _manaByGuid.ContainsKey(guid);
|
||||
public int Count => _manaByGuid.Count;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
|
||||
public bool TryGetManaPercent(uint guid, out float fraction) =>
|
||||
_manaByGuid.TryGetValue(guid, out fraction);
|
||||
|
||||
public void OnQueryItemManaResponse(uint itemGuid, float manaPercent, bool valid)
|
||||
{
|
||||
|
|
@ -28,8 +34,13 @@ public sealed class ItemManaState
|
|||
else
|
||||
_manaByGuid.TryRemove(itemGuid, out _);
|
||||
|
||||
Interlocked.Increment(ref _revision);
|
||||
ItemManaChanged?.Invoke(itemGuid, manaPercent, valid);
|
||||
}
|
||||
|
||||
public void Clear() => _manaByGuid.Clear();
|
||||
public void Clear()
|
||||
{
|
||||
_manaByGuid.Clear();
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,29 @@ public sealed class FriendsState
|
|||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly List<FriendEntry> _entries = new();
|
||||
private long _revision;
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { lock (_gate) return _entries.Count; }
|
||||
}
|
||||
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
|
||||
public IReadOnlyList<FriendEntry> Snapshot()
|
||||
{
|
||||
lock (_gate) return _entries.ToArray();
|
||||
}
|
||||
|
||||
public bool TryGet(uint characterId, out FriendEntry? friend)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
friend = _entries.Find(candidate => candidate.Id == characterId);
|
||||
return friend is not null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(FriendsUpdate update)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(update);
|
||||
|
|
@ -43,12 +60,17 @@ public sealed class FriendsState
|
|||
}
|
||||
break;
|
||||
}
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_gate) _entries.Clear();
|
||||
lock (_gate)
|
||||
{
|
||||
_entries.Clear();
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ public sealed class SquelchState
|
|||
{
|
||||
private readonly object _gate = new();
|
||||
private SquelchDatabase _database = SquelchDatabase.Empty;
|
||||
private long _revision;
|
||||
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
|
||||
public SquelchDatabase Snapshot()
|
||||
{
|
||||
|
|
@ -14,7 +17,11 @@ public sealed class SquelchState
|
|||
public void Replace(SquelchDatabase database)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
lock (_gate) _database = database;
|
||||
lock (_gate)
|
||||
{
|
||||
_database = database;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -25,7 +32,11 @@ public sealed class SquelchState
|
|||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_gate) _database = SquelchDatabase.Empty;
|
||||
lock (_gate)
|
||||
{
|
||||
_database = SquelchDatabase.Empty;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,114 @@ public interface IRuntimePortalCommands
|
|||
RuntimePortalCommand command);
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeShortcutCommand(
|
||||
int Index,
|
||||
uint ObjectId,
|
||||
uint SpellId);
|
||||
|
||||
public interface IRuntimeInventoryStateCommands
|
||||
{
|
||||
RuntimeCommandResult AddShortcut(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeShortcutCommand command);
|
||||
|
||||
RuntimeCommandResult RemoveShortcut(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int index);
|
||||
}
|
||||
|
||||
public interface IRuntimeSpellbookCommands
|
||||
{
|
||||
RuntimeCommandResult AddFavorite(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int tabIndex,
|
||||
int position,
|
||||
uint spellId);
|
||||
|
||||
RuntimeCommandResult RemoveFavorite(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int tabIndex,
|
||||
uint spellId);
|
||||
|
||||
RuntimeCommandResult SetFilter(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint filters);
|
||||
|
||||
RuntimeCommandResult ForgetSpell(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint spellId);
|
||||
|
||||
RuntimeCommandResult SetDesiredComponent(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint componentId,
|
||||
uint amount);
|
||||
|
||||
RuntimeCommandResult ClearDesiredComponents(
|
||||
RuntimeGenerationToken expectedGeneration);
|
||||
}
|
||||
|
||||
public enum RuntimeAdvancementKind
|
||||
{
|
||||
Attribute,
|
||||
Vital,
|
||||
Skill,
|
||||
TrainSkill,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeAdvancementCommand(
|
||||
RuntimeAdvancementKind Kind,
|
||||
uint StatId,
|
||||
ulong Cost);
|
||||
|
||||
public interface IRuntimeCharacterCommands
|
||||
{
|
||||
RuntimeCommandResult Advance(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeAdvancementCommand command);
|
||||
|
||||
RuntimeCommandResult SetOptions1(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint options);
|
||||
}
|
||||
|
||||
public enum RuntimeFriendCommandKind
|
||||
{
|
||||
Add,
|
||||
Remove,
|
||||
Clear,
|
||||
RequestLegacyList,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeFriendCommand(
|
||||
RuntimeFriendCommandKind Kind,
|
||||
uint CharacterId = 0u,
|
||||
string? Name = null);
|
||||
|
||||
public enum RuntimeSquelchScope
|
||||
{
|
||||
Character,
|
||||
Account,
|
||||
Global,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeSquelchCommand(
|
||||
RuntimeSquelchScope Scope,
|
||||
bool Add,
|
||||
uint CharacterId = 0u,
|
||||
string? Name = null,
|
||||
uint MessageType = 0u);
|
||||
|
||||
public interface IRuntimeSocialCommands
|
||||
{
|
||||
RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeFriendCommand command);
|
||||
|
||||
RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeSquelchCommand command);
|
||||
}
|
||||
|
||||
public interface IGameRuntimeCommands
|
||||
{
|
||||
IRuntimeSessionCommands Session { get; }
|
||||
|
|
@ -148,4 +256,12 @@ public interface IGameRuntimeCommands
|
|||
IRuntimeChatCommands Chat { get; }
|
||||
|
||||
IRuntimePortalCommands Portal { get; }
|
||||
|
||||
IRuntimeInventoryStateCommands InventoryState { get; }
|
||||
|
||||
IRuntimeSpellbookCommands Spellbook { get; }
|
||||
|
||||
IRuntimeCharacterCommands Character { get; }
|
||||
|
||||
IRuntimeSocialCommands Social { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ public enum RuntimeCommandDomain
|
|||
Movement,
|
||||
Chat,
|
||||
Portal,
|
||||
InventoryState,
|
||||
Spellbook,
|
||||
Character,
|
||||
Social,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeLifecycleDelta(
|
||||
|
|
@ -145,6 +149,19 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
|||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
$"frame={checkpoint.FrameNumber};materialized={checkpoint.MaterializedEntityCount};" +
|
||||
$"containers={checkpoint.InventoryContainerCount};chat={checkpoint.ChatCount};" +
|
||||
$"inventory-state={checkpoint.InventoryState.ShortcutCount}:" +
|
||||
$"{checkpoint.InventoryState.ShortcutRevision}:" +
|
||||
$"{checkpoint.InventoryState.ItemManaCount}:" +
|
||||
$"{checkpoint.InventoryState.ItemManaRevision};" +
|
||||
$"character={checkpoint.Character.CharacterRevision}:" +
|
||||
$"{checkpoint.Character.SpellbookRevision}:" +
|
||||
$"{checkpoint.Character.LearnedSpellCount}:" +
|
||||
$"{checkpoint.Character.SkillCount}:" +
|
||||
$"{checkpoint.Character.Options.Revision}:" +
|
||||
$"{checkpoint.Character.MovementSkills.Revision};" +
|
||||
$"social={checkpoint.Social.FriendsRevision}:" +
|
||||
$"{checkpoint.Social.FriendCount}:" +
|
||||
$"{checkpoint.Social.SquelchRevision};" +
|
||||
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
||||
}
|
||||
|
|
|
|||
110
src/AcDream.Runtime/GameRuntimeGameplayViews.cs
Normal file
110
src/AcDream.Runtime/GameRuntimeGameplayViews.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
public readonly record struct RuntimePendingInventoryRequestSnapshot(
|
||||
ulong Token,
|
||||
int Kind,
|
||||
uint ItemId,
|
||||
bool Dispatched);
|
||||
|
||||
public readonly record struct RuntimeInventoryStateSnapshot(
|
||||
uint RequestedExternalContainerId,
|
||||
uint CurrentExternalContainerId,
|
||||
int BusyCount,
|
||||
bool CanBeginRequest,
|
||||
RuntimePendingInventoryRequestSnapshot? PendingRequest,
|
||||
int ShortcutCount,
|
||||
long ShortcutRevision,
|
||||
int ItemManaCount,
|
||||
long ItemManaRevision);
|
||||
|
||||
public readonly record struct RuntimeShortcutSnapshot(
|
||||
int Index,
|
||||
uint ObjectId,
|
||||
uint SpellId);
|
||||
|
||||
public interface IRuntimeInventoryStateView
|
||||
{
|
||||
RuntimeInventoryStateSnapshot Snapshot { get; }
|
||||
|
||||
bool TryGetShortcut(int index, out RuntimeShortcutSnapshot shortcut);
|
||||
|
||||
bool TryGetItemMana(uint objectId, out float fraction);
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeCharacterSnapshot(
|
||||
long CharacterRevision,
|
||||
long SpellbookRevision,
|
||||
RuntimeCharacterOptionsSnapshot Options,
|
||||
RuntimeMovementSkillSnapshot MovementSkills,
|
||||
int LearnedSpellCount,
|
||||
int ActiveEnchantmentCount,
|
||||
int DesiredComponentCount,
|
||||
int SkillCount,
|
||||
uint SpellbookFilters);
|
||||
|
||||
public readonly record struct RuntimeVitalSnapshot(
|
||||
int Kind,
|
||||
uint Ranks,
|
||||
uint Start,
|
||||
uint Experience,
|
||||
uint Current,
|
||||
uint Maximum);
|
||||
|
||||
public readonly record struct RuntimeAttributeSnapshot(
|
||||
int Kind,
|
||||
uint Ranks,
|
||||
uint Start,
|
||||
uint Experience,
|
||||
uint Current);
|
||||
|
||||
public readonly record struct RuntimeSkillSnapshot(
|
||||
uint SkillId,
|
||||
uint Ranks,
|
||||
uint Status,
|
||||
uint Experience,
|
||||
uint Initial,
|
||||
uint Resistance,
|
||||
double LastUsed,
|
||||
uint FormulaBonus,
|
||||
uint CurrentLevel);
|
||||
|
||||
public interface IRuntimeCharacterView
|
||||
{
|
||||
RuntimeCharacterSnapshot Snapshot { get; }
|
||||
|
||||
bool TryGetVital(int kind, out RuntimeVitalSnapshot vital);
|
||||
|
||||
bool TryGetAttribute(int kind, out RuntimeAttributeSnapshot attribute);
|
||||
|
||||
bool TryGetSkill(uint skillId, out RuntimeSkillSnapshot skill);
|
||||
|
||||
bool KnowsSpell(uint spellId);
|
||||
|
||||
bool TryGetFavorite(int tabIndex, int position, out uint spellId);
|
||||
|
||||
bool TryGetDesiredComponent(uint componentId, out uint amount);
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeSocialSnapshot(
|
||||
long FriendsRevision,
|
||||
int FriendCount,
|
||||
long SquelchRevision,
|
||||
int SquelchedAccountCount,
|
||||
int SquelchedCharacterCount,
|
||||
int GlobalSquelchTypeCount,
|
||||
int NegotiatedChatRoomCount);
|
||||
|
||||
public readonly record struct RuntimeFriendSnapshot(
|
||||
uint Id,
|
||||
string Name,
|
||||
bool Online,
|
||||
bool AppearOffline);
|
||||
|
||||
public interface IRuntimeSocialView
|
||||
{
|
||||
RuntimeSocialSnapshot Snapshot { get; }
|
||||
|
||||
bool TryGetFriend(uint characterId, out RuntimeFriendSnapshot friend);
|
||||
}
|
||||
|
|
@ -110,6 +110,9 @@ public readonly record struct RuntimeStateCheckpoint(
|
|||
int MaterializedEntityCount,
|
||||
int InventoryObjectCount,
|
||||
int InventoryContainerCount,
|
||||
RuntimeInventoryStateSnapshot InventoryState,
|
||||
RuntimeCharacterSnapshot Character,
|
||||
RuntimeSocialSnapshot Social,
|
||||
long ChatRevision,
|
||||
int ChatCount,
|
||||
RuntimeMovementSnapshot Movement,
|
||||
|
|
@ -127,6 +130,12 @@ public interface IGameRuntimeView
|
|||
|
||||
IRuntimeInventoryView Inventory { get; }
|
||||
|
||||
IRuntimeInventoryStateView InventoryState { get; }
|
||||
|
||||
IRuntimeCharacterView Character { get; }
|
||||
|
||||
IRuntimeSocialView Social { get; }
|
||||
|
||||
IRuntimeChatView Chat { get; }
|
||||
|
||||
IRuntimeMovementView Movement { get; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
|
@ -11,15 +12,27 @@ namespace AcDream.Runtime.Gameplay;
|
|||
public sealed class RuntimeCharacterState : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
private long _characterRevision;
|
||||
private long _spellbookRevision;
|
||||
|
||||
public RuntimeCharacterState(SpellTable? spellTable = null)
|
||||
{
|
||||
Spellbook = new Spellbook(spellTable);
|
||||
LocalPlayer = new LocalPlayerState(Spellbook);
|
||||
Options = new RuntimeCharacterOptionsState();
|
||||
MovementSkills = new RuntimeMovementSkillState();
|
||||
View = new CharacterView(this);
|
||||
Spellbook.StateChanged += OnSpellbookChanged;
|
||||
LocalPlayer.Changed += OnVitalChanged;
|
||||
LocalPlayer.AttributeChanged += OnAttributeChanged;
|
||||
LocalPlayer.CharacterChanged += OnCharacterChanged;
|
||||
}
|
||||
|
||||
public Spellbook Spellbook { get; }
|
||||
public LocalPlayerState LocalPlayer { get; }
|
||||
public RuntimeCharacterOptionsState Options { get; }
|
||||
public RuntimeMovementSkillState MovementSkills { get; }
|
||||
public IRuntimeCharacterView View { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -55,6 +68,8 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
List<Exception>? failures = null;
|
||||
Try(Spellbook.Clear, ref failures);
|
||||
Try(LocalPlayer.Clear, ref failures);
|
||||
Try(Options.ResetSession, ref failures);
|
||||
Try(MovementSkills.ResetSession, ref failures);
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
|
|
@ -68,9 +83,25 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
if (_disposed)
|
||||
return;
|
||||
ResetSession();
|
||||
Spellbook.StateChanged -= OnSpellbookChanged;
|
||||
LocalPlayer.Changed -= OnVitalChanged;
|
||||
LocalPlayer.AttributeChanged -= OnAttributeChanged;
|
||||
LocalPlayer.CharacterChanged -= OnCharacterChanged;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void OnSpellbookChanged() =>
|
||||
Interlocked.Increment(ref _spellbookRevision);
|
||||
|
||||
private void OnVitalChanged(LocalPlayerState.VitalKind _) =>
|
||||
Interlocked.Increment(ref _characterRevision);
|
||||
|
||||
private void OnAttributeChanged(LocalPlayerState.AttributeKind _) =>
|
||||
Interlocked.Increment(ref _characterRevision);
|
||||
|
||||
private void OnCharacterChanged() =>
|
||||
Interlocked.Increment(ref _characterRevision);
|
||||
|
||||
private static void Try(Action action, ref List<Exception>? failures)
|
||||
{
|
||||
try
|
||||
|
|
@ -82,4 +113,212 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CharacterView(RuntimeCharacterState owner)
|
||||
: IRuntimeCharacterView
|
||||
{
|
||||
public RuntimeCharacterSnapshot Snapshot => new(
|
||||
Interlocked.Read(ref owner._characterRevision),
|
||||
Interlocked.Read(ref owner._spellbookRevision),
|
||||
owner.Options.Snapshot,
|
||||
owner.MovementSkills.Snapshot,
|
||||
owner.Spellbook.LearnedSpells.Count,
|
||||
owner.Spellbook.ActiveEnchantments.Count(),
|
||||
owner.Spellbook.DesiredComponents.Count,
|
||||
owner.LocalPlayer.Skills.Count,
|
||||
owner.Spellbook.SpellbookFilters);
|
||||
|
||||
public bool TryGetVital(int kind, out RuntimeVitalSnapshot vital)
|
||||
{
|
||||
if (!Enum.IsDefined((LocalPlayerState.VitalKind)kind)
|
||||
|| owner.LocalPlayer.Get((LocalPlayerState.VitalKind)kind)
|
||||
is not LocalPlayerState.VitalSnapshot current)
|
||||
{
|
||||
vital = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
vital = new RuntimeVitalSnapshot(
|
||||
kind,
|
||||
current.Ranks,
|
||||
current.Start,
|
||||
current.Xp,
|
||||
current.Current,
|
||||
owner.LocalPlayer.GetMaxApprox(
|
||||
(LocalPlayerState.VitalKind)kind) ?? 0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetAttribute(
|
||||
int kind,
|
||||
out RuntimeAttributeSnapshot attribute)
|
||||
{
|
||||
if (!Enum.IsDefined((LocalPlayerState.AttributeKind)kind)
|
||||
|| owner.LocalPlayer.GetAttribute(
|
||||
(LocalPlayerState.AttributeKind)kind)
|
||||
is not LocalPlayerState.AttributeSnapshot current)
|
||||
{
|
||||
attribute = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
attribute = new RuntimeAttributeSnapshot(
|
||||
kind,
|
||||
current.Ranks,
|
||||
current.Start,
|
||||
current.Xp,
|
||||
current.Current);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetSkill(uint skillId, out RuntimeSkillSnapshot skill)
|
||||
{
|
||||
if (owner.LocalPlayer.GetSkill(skillId)
|
||||
is not LocalPlayerState.SkillSnapshot current)
|
||||
{
|
||||
skill = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
skill = new RuntimeSkillSnapshot(
|
||||
current.SkillId,
|
||||
current.Ranks,
|
||||
current.Status,
|
||||
current.Xp,
|
||||
current.Init,
|
||||
current.Resistance,
|
||||
current.LastUsed,
|
||||
current.FormulaBonus,
|
||||
current.CurrentLevel);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool KnowsSpell(uint spellId) =>
|
||||
owner.Spellbook.Knows(spellId);
|
||||
|
||||
public bool TryGetFavorite(
|
||||
int tabIndex,
|
||||
int position,
|
||||
out uint spellId)
|
||||
{
|
||||
IReadOnlyList<uint> favorites =
|
||||
owner.Spellbook.GetFavorites(tabIndex);
|
||||
if ((uint)position >= (uint)favorites.Count)
|
||||
{
|
||||
spellId = 0u;
|
||||
return false;
|
||||
}
|
||||
spellId = favorites[position];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetDesiredComponent(
|
||||
uint componentId,
|
||||
out uint amount) =>
|
||||
owner.Spellbook.DesiredComponents.TryGetValue(
|
||||
componentId,
|
||||
out amount);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeCharacterOptionsSnapshot(
|
||||
uint Options1,
|
||||
uint Options2,
|
||||
long Revision)
|
||||
{
|
||||
public bool DragItemOnPlayerOpensSecureTrade =>
|
||||
(Options1
|
||||
& (uint)PlayerDescriptionParser.CharacterOptions1
|
||||
.DragItemOnPlayerOpensSecureTrade) != 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Canonical session-owned copy of retail's two character-option bitfields.
|
||||
/// <c>PlayerModule::PlayerModule @ 0x005D51F0</c> installs the defaults.
|
||||
/// Runtime reset restores the equivalent fresh-player-module state because
|
||||
/// one Runtime owner survives across graphical and no-window sessions.
|
||||
/// </summary>
|
||||
public sealed class RuntimeCharacterOptionsState
|
||||
{
|
||||
public const uint DefaultOptions1 =
|
||||
(uint)PlayerDescriptionParser.CharacterOptions1.Default;
|
||||
public const uint DefaultOptions2 = 0x00948700u;
|
||||
|
||||
private uint _options1 = DefaultOptions1;
|
||||
private uint _options2 = DefaultOptions2;
|
||||
private long _revision;
|
||||
|
||||
public uint Options1 => Volatile.Read(ref _options1);
|
||||
public uint Options2 => Volatile.Read(ref _options2);
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public RuntimeCharacterOptionsSnapshot Snapshot =>
|
||||
new(_options1, _options2, Revision);
|
||||
|
||||
public bool DragItemOnPlayerOpensSecureTrade =>
|
||||
Snapshot.DragItemOnPlayerOpensSecureTrade;
|
||||
|
||||
public void Replace(uint options1, uint options2)
|
||||
{
|
||||
Volatile.Write(ref _options1, options1);
|
||||
Volatile.Write(ref _options2, options2);
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
Volatile.Write(ref _options1, DefaultOptions1);
|
||||
Volatile.Write(ref _options2, DefaultOptions2);
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeMovementSkillSnapshot(
|
||||
int RunSkill,
|
||||
int JumpSkill,
|
||||
long Revision)
|
||||
{
|
||||
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-authoritative run/jump values retained independently of any
|
||||
/// graphical movement controller. App applies this borrowed state whenever
|
||||
/// its presentation/physics controller exists or is rebuilt.
|
||||
/// </summary>
|
||||
public sealed class RuntimeMovementSkillState
|
||||
{
|
||||
private int _runSkill = -1;
|
||||
private int _jumpSkill = -1;
|
||||
private long _revision;
|
||||
|
||||
public int RunSkill => Volatile.Read(ref _runSkill);
|
||||
public int JumpSkill => Volatile.Read(ref _jumpSkill);
|
||||
public bool IsComplete => _runSkill >= 0 && _jumpSkill >= 0;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public RuntimeMovementSkillSnapshot Snapshot =>
|
||||
new(_runSkill, _jumpSkill, Revision);
|
||||
|
||||
public void Update(int runSkill, int jumpSkill)
|
||||
{
|
||||
bool changed = false;
|
||||
if (runSkill >= 0 && RunSkill != runSkill)
|
||||
{
|
||||
Volatile.Write(ref _runSkill, runSkill);
|
||||
changed = true;
|
||||
}
|
||||
if (jumpSkill >= 0 && JumpSkill != jumpSkill)
|
||||
{
|
||||
Volatile.Write(ref _jumpSkill, jumpSkill);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
Volatile.Write(ref _runSkill, -1);
|
||||
Volatile.Write(ref _jumpSkill, -1);
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ public sealed class RuntimeCommunicationState : IDisposable
|
|||
Friends = new FriendsState();
|
||||
Squelch = new SquelchState();
|
||||
View = new CommunicationView(Chat);
|
||||
SocialView = new CommunicationSocialView(
|
||||
TurbineChat,
|
||||
Friends,
|
||||
Squelch);
|
||||
}
|
||||
|
||||
public ChatLog Chat { get; }
|
||||
|
|
@ -44,6 +48,7 @@ public sealed class RuntimeCommunicationState : IDisposable
|
|||
public FriendsState Friends { get; }
|
||||
public SquelchState Squelch { get; }
|
||||
public IRuntimeChatView View { get; }
|
||||
public IRuntimeSocialView SocialView { get; }
|
||||
public IRuntimeCommunicationEventSource Events => _events;
|
||||
|
||||
public bool IsDisposed => _disposed;
|
||||
|
|
@ -79,6 +84,61 @@ public sealed class RuntimeCommunicationState : IDisposable
|
|||
public long Revision => chat.Revision;
|
||||
public int Count => chat.Count;
|
||||
}
|
||||
|
||||
private sealed class CommunicationSocialView(
|
||||
TurbineChatState turbineChat,
|
||||
FriendsState friends,
|
||||
SquelchState squelch)
|
||||
: IRuntimeSocialView
|
||||
{
|
||||
public RuntimeSocialSnapshot Snapshot
|
||||
{
|
||||
get
|
||||
{
|
||||
SquelchDatabase database = squelch.Snapshot();
|
||||
return new RuntimeSocialSnapshot(
|
||||
friends.Revision,
|
||||
friends.Count,
|
||||
squelch.Revision,
|
||||
database.Accounts.Count,
|
||||
database.Characters.Count,
|
||||
database.Global.MessageTypes.Count,
|
||||
CountRooms(turbineChat));
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetFriend(
|
||||
uint characterId,
|
||||
out RuntimeFriendSnapshot friend)
|
||||
{
|
||||
if (!friends.TryGet(characterId, out FriendEntry? current)
|
||||
|| current is null)
|
||||
{
|
||||
friend = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
friend = new RuntimeFriendSnapshot(
|
||||
current.Id,
|
||||
current.Name,
|
||||
current.Online,
|
||||
current.AppearOffline);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int CountRooms(TurbineChatState state)
|
||||
{
|
||||
int count = 0;
|
||||
if (state.AllegianceRoom != 0u) count++;
|
||||
if (state.GeneralRoom != 0u) count++;
|
||||
if (state.TradeRoom != 0u) count++;
|
||||
if (state.LfgRoom != 0u) count++;
|
||||
if (state.RoleplayRoom != 0u) count++;
|
||||
if (state.SocietyRoom != 0u) count++;
|
||||
if (state.OlthoiRoom != 0u) count++;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RuntimeCommunicationEventStream
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
ItemMana = new ItemManaState();
|
||||
Shortcuts = new ShortcutState();
|
||||
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
||||
View = new InventoryStateView(this);
|
||||
}
|
||||
|
||||
public ClientObjectTable Objects => _entityObjects.Objects;
|
||||
|
|
@ -28,6 +29,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
public ItemManaState ItemMana { get; }
|
||||
public ShortcutState Shortcuts { get; }
|
||||
public InventoryTransactionState Transactions { get; }
|
||||
public IRuntimeInventoryStateView View { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
public void ResetExternalContainer() => ExternalContainers.Reset();
|
||||
|
|
@ -66,6 +68,61 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InventoryStateView(RuntimeInventoryState owner)
|
||||
: IRuntimeInventoryStateView
|
||||
{
|
||||
public RuntimeInventoryStateSnapshot Snapshot
|
||||
{
|
||||
get
|
||||
{
|
||||
RuntimePendingInventoryRequestSnapshot? pending = null;
|
||||
if (owner.Transactions.TryGetPending(
|
||||
out PendingInventoryRequest request))
|
||||
{
|
||||
pending = new RuntimePendingInventoryRequestSnapshot(
|
||||
request.Token,
|
||||
(int)request.Kind,
|
||||
request.ItemId,
|
||||
request.Dispatched);
|
||||
}
|
||||
|
||||
return new RuntimeInventoryStateSnapshot(
|
||||
owner.ExternalContainers.RequestedContainerId,
|
||||
owner.ExternalContainers.CurrentContainerId,
|
||||
owner.Transactions.BusyCount,
|
||||
owner.Transactions.CanBeginRequest,
|
||||
pending,
|
||||
owner.Shortcuts.Items.Count,
|
||||
owner.Shortcuts.Revision,
|
||||
owner.ItemMana.Count,
|
||||
owner.ItemMana.Revision);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetShortcut(
|
||||
int index,
|
||||
out RuntimeShortcutSnapshot shortcut)
|
||||
{
|
||||
IReadOnlyList<ShortcutEntry> shortcuts = owner.Shortcuts.Items;
|
||||
for (int i = 0; i < shortcuts.Count; i++)
|
||||
{
|
||||
if (shortcuts[i].Index != index)
|
||||
continue;
|
||||
ShortcutEntry current = shortcuts[i];
|
||||
shortcut = new RuntimeShortcutSnapshot(
|
||||
current.Index,
|
||||
current.ObjectId,
|
||||
current.SpellId);
|
||||
return true;
|
||||
}
|
||||
shortcut = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetItemMana(uint objectId, out float fraction) =>
|
||||
owner.ItemMana.TryGetManaPercent(objectId, out fraction);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ShortcutState
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ public sealed record LiveCharacterSessionBindings(
|
|||
Action<int, int>? OnSkillsUpdated,
|
||||
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
||||
Action<GameEvents.CharacterConfirmationDone>? OnConfirmationDone,
|
||||
Action<uint, uint>? OnCharacterOptions,
|
||||
Func<double>? ClientTime);
|
||||
|
||||
public sealed record LiveSocialSessionBindings(
|
||||
|
|
@ -152,7 +151,13 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
social.Chat,
|
||||
character.Character.LocalPlayer,
|
||||
social.TurbineChat,
|
||||
onSkillsUpdated: character.OnSkillsUpdated,
|
||||
onSkillsUpdated: (runSkill, jumpSkill) =>
|
||||
{
|
||||
character.Character.MovementSkills.Update(
|
||||
runSkill,
|
||||
jumpSkill);
|
||||
character.OnSkillsUpdated?.Invoke(runSkill, jumpSkill);
|
||||
},
|
||||
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
||||
onShortcuts: inventory.OnShortcuts,
|
||||
playerGuid: inventory.PlayerGuid,
|
||||
|
|
@ -164,7 +169,7 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
friends: social.Friends,
|
||||
squelch: social.Squelch,
|
||||
onDesiredComponents: null,
|
||||
onCharacterOptions: character.OnCharacterOptions,
|
||||
onCharacterOptions: character.Character.Options.Replace,
|
||||
clientTime: character.ClientTime,
|
||||
externalContainers: inventory.ExternalContainers,
|
||||
accepting: IsAccepting));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue