refactor(runtime): own communication and social state
Construct chat history, negotiated channels, friends, squelch, and reply targets in one presentation-independent Runtime owner. Make live routing, retained UI, devtools, and current-runtime projections borrow the exact instances, preserve reconnect reset semantics, and publish failure-isolated reentrant-safe chat commits. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
e321410770
commit
c9d25ade50
19 changed files with 684 additions and 117 deletions
|
|
@ -18,6 +18,7 @@ using AcDream.Core.Items;
|
||||||
using AcDream.Core.Player;
|
using AcDream.Core.Player;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
using AcDream.UI.Abstractions.Panels.Chat;
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||||
|
|
@ -49,7 +50,7 @@ internal sealed record InteractionRetainedUiDependencies(
|
||||||
ClientObjectTable Objects,
|
ClientObjectTable Objects,
|
||||||
MagicCatalog MagicCatalog,
|
MagicCatalog MagicCatalog,
|
||||||
Spellbook Spellbook,
|
Spellbook Spellbook,
|
||||||
ChatLog Chat,
|
RuntimeCommunicationState Communication,
|
||||||
LocalPlayerState LocalPlayer,
|
LocalPlayerState LocalPlayer,
|
||||||
ItemManaState ItemMana,
|
ItemManaState ItemMana,
|
||||||
StackSplitQuantityState StackSplitQuantity,
|
StackSplitQuantityState StackSplitQuantity,
|
||||||
|
|
@ -317,7 +318,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
session.CurrentSession?.SendStackableSplitTo3D(item, amount),
|
session.CurrentSession?.SendStackableSplitTo3D(item, amount),
|
||||||
selectedObjectId: () => d.Selection.SelectedObjectId ?? 0u,
|
selectedObjectId: () => d.Selection.SelectedObjectId ?? 0u,
|
||||||
stackSplitQuantity: d.StackSplitQuantity,
|
stackSplitQuantity: d.StackSplitQuantity,
|
||||||
systemMessage: text => d.Chat.OnSystemMessage(text, 0x1Au),
|
systemMessage:
|
||||||
|
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||||
sendPutItemInContainer: (item, container, placement) =>
|
sendPutItemInContainer: (item, container, placement) =>
|
||||||
session.CurrentSession?.SendPutItemInContainer(
|
session.CurrentSession?.SendPutItemInContainer(
|
||||||
item,
|
item,
|
||||||
|
|
@ -400,7 +402,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
late.Session.CurrentSession?.SendCastUntargetedSpell(spellId),
|
late.Session.CurrentSession?.SendCastUntargetedSpell(spellId),
|
||||||
sendTargeted: (target, spellId) =>
|
sendTargeted: (target, spellId) =>
|
||||||
late.Session.CurrentSession?.SendCastTargetedSpell(target, spellId),
|
late.Session.CurrentSession?.SendCastTargetedSpell(target, spellId),
|
||||||
displayMessage: text => d.Chat.OnSystemMessage(text, 0x1Au),
|
displayMessage:
|
||||||
|
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||||
canSend: () => late.Session.IsInWorld);
|
canSend: () => late.Session.IsInWorld);
|
||||||
checkpoint(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
checkpoint(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
||||||
|
|
@ -477,7 +480,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
|
|
||||||
host.Root.Width = d.Window.Size.X;
|
host.Root.Width = d.Window.Size.X;
|
||||||
host.Root.Height = d.Window.Size.Y;
|
host.Root.Height = d.Window.Size.Y;
|
||||||
var chat = new ChatVM(d.Chat, displayLimit: 200);
|
var chat = new ChatVM(
|
||||||
|
d.Communication.Chat,
|
||||||
|
displayLimit: 200,
|
||||||
|
commandTargets: d.Communication.CommandTargets);
|
||||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore =
|
AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore =
|
||||||
d.Settings.LayoutStore;
|
d.Settings.LayoutStore;
|
||||||
RetailUiPersistenceBindings? persistence = layoutStore is null
|
RetailUiPersistenceBindings? persistence = layoutStore is null
|
||||||
|
|
@ -663,7 +669,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
late.Session.CurrentSession?.SendSetInscription(
|
late.Session.CurrentSession?.SendSetInscription(
|
||||||
item,
|
item,
|
||||||
inscription),
|
inscription),
|
||||||
text => d.Chat.OnSystemMessage(text, 0x1Au)),
|
text =>
|
||||||
|
d.Communication.Chat.OnSystemMessage(text, 0x1Au)),
|
||||||
StackSplitQuantity: d.StackSplitQuantity,
|
StackSplitQuantity: d.StackSplitQuantity,
|
||||||
Plugins: d.UiRegistry,
|
Plugins: d.UiRegistry,
|
||||||
Persistence: persistence,
|
Persistence: persistence,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ using AcDream.Core.Social;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
|
|
@ -84,13 +85,10 @@ internal sealed record SessionPlayerDependencies(
|
||||||
CombatAttackOperationsSlot CombatAttackOperations,
|
CombatAttackOperationsSlot CombatAttackOperations,
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
CombatFeedbackSlot CombatFeedback,
|
CombatFeedbackSlot CombatFeedback,
|
||||||
ChatLog Chat,
|
RuntimeCommunicationState Communication,
|
||||||
LocalPlayerState LocalPlayer,
|
LocalPlayerState LocalPlayer,
|
||||||
Spellbook Spellbook,
|
Spellbook Spellbook,
|
||||||
ItemManaState ItemMana,
|
ItemManaState ItemMana,
|
||||||
FriendsState Friends,
|
|
||||||
SquelchState Squelch,
|
|
||||||
TurbineChatState TurbineChat,
|
|
||||||
ExternalContainerState ExternalContainers,
|
ExternalContainerState ExternalContainers,
|
||||||
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||||
Action<string> Log);
|
Action<string> Log);
|
||||||
|
|
@ -798,8 +796,6 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
||||||
d.SettingsDevTools.DevTools?.Vitals
|
d.SettingsDevTools.DevTools?.Vitals
|
||||||
?? interaction.RetainedUi?.Vitals;
|
?? interaction.RetainedUi?.Vitals;
|
||||||
AcDream.UI.Abstractions.Panels.Chat.ChatVM? retailChat =
|
|
||||||
interaction.RetainedUi?.Chat;
|
|
||||||
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
||||||
new LiveSessionPlayerRuntime(
|
new LiveSessionPlayerRuntime(
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
|
|
@ -815,15 +811,11 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.Spellbook,
|
d.Spellbook,
|
||||||
d.Combat,
|
d.Combat,
|
||||||
d.ItemMana,
|
d.ItemMana,
|
||||||
d.Friends,
|
|
||||||
d.Squelch,
|
|
||||||
d.TurbineChat,
|
|
||||||
d.ExternalContainers,
|
d.ExternalContainers,
|
||||||
d.Chat),
|
d.Communication),
|
||||||
new LiveSessionUiRuntime(
|
new LiveSessionUiRuntime(
|
||||||
interaction.RetainedUi?.Runtime,
|
interaction.RetainedUi?.Runtime,
|
||||||
vitals,
|
vitals,
|
||||||
retailChat,
|
|
||||||
interaction.RetainedUi?.CharacterSheet,
|
interaction.RetainedUi?.CharacterSheet,
|
||||||
interaction.RetainedUi?.Magic,
|
interaction.RetainedUi?.Magic,
|
||||||
live.PaperdollPresenter),
|
live.PaperdollPresenter),
|
||||||
|
|
@ -881,7 +873,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
interaction.ItemInteraction),
|
interaction.ItemInteraction),
|
||||||
d.Log,
|
d.Log,
|
||||||
debugToast,
|
debugToast,
|
||||||
text => d.Chat.OnSystemMessage(text, 0x1Au));
|
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au));
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
"live combat-mode commands",
|
"live combat-mode commands",
|
||||||
d.CombatModeCommands.BindOwned(combatCommand));
|
d.CombatModeCommands.BindOwned(combatCommand));
|
||||||
|
|
@ -892,7 +884,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
live.LiveEntities,
|
live.LiveEntities,
|
||||||
d.EntityObjects,
|
d.EntityObjects,
|
||||||
d.Chat,
|
d.Communication,
|
||||||
d.PlayerController,
|
d.PlayerController,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
d.UpdateClock,
|
d.UpdateClock,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Settings;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Player;
|
using AcDream.Core.Player;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
using AcDream.UI.Abstractions.Panels.Chat;
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
using AcDream.UI.Abstractions.Panels.Debug;
|
using AcDream.UI.Abstractions.Panels.Debug;
|
||||||
|
|
@ -34,7 +35,7 @@ internal sealed record SettingsDevToolsDependencies(
|
||||||
RuntimeSettingsController Settings,
|
RuntimeSettingsController Settings,
|
||||||
IRuntimeSettingsStartupTarget StartupTarget,
|
IRuntimeSettingsStartupTarget StartupTarget,
|
||||||
HostQuiescenceGate HostQuiescence,
|
HostQuiescenceGate HostQuiescence,
|
||||||
ChatLog Chat,
|
RuntimeCommunicationState Communication,
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
LocalPlayerState LocalPlayer,
|
LocalPlayerState LocalPlayer,
|
||||||
SettingsDevToolsOptionalDependencies? DevTools,
|
SettingsDevToolsOptionalDependencies? DevTools,
|
||||||
|
|
@ -418,7 +419,10 @@ internal sealed class SettingsDevToolsCompositionPhase :
|
||||||
|
|
||||||
var chatLease = scope.Acquire(
|
var chatLease = scope.Acquire(
|
||||||
"developer chat view model",
|
"developer chat view model",
|
||||||
() => new ChatVM(_dependencies.Chat)
|
() => new ChatVM(
|
||||||
|
_dependencies.Communication.Chat,
|
||||||
|
commandTargets:
|
||||||
|
_dependencies.Communication.CommandTargets)
|
||||||
{
|
{
|
||||||
FpsProvider = () => optional.Facts.Fps,
|
FpsProvider = () => optional.Facts.Fps,
|
||||||
PositionProvider = () => optional.Facts.PlayerPosition,
|
PositionProvider = () => optional.Facts.PlayerPosition,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ using AcDream.Core.Spells;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions.Panels.Chat;
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||||
|
|
@ -47,16 +48,12 @@ internal sealed record LiveSessionDomainRuntime(
|
||||||
Spellbook Spellbook,
|
Spellbook Spellbook,
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
ItemManaState ItemMana,
|
ItemManaState ItemMana,
|
||||||
FriendsState Friends,
|
|
||||||
SquelchState Squelch,
|
|
||||||
TurbineChatState TurbineChat,
|
|
||||||
ExternalContainerState ExternalContainers,
|
ExternalContainerState ExternalContainers,
|
||||||
ChatLog Chat);
|
RuntimeCommunicationState Communication);
|
||||||
|
|
||||||
internal sealed record LiveSessionUiRuntime(
|
internal sealed record LiveSessionUiRuntime(
|
||||||
RetailUiRuntime? RetailUi,
|
RetailUiRuntime? RetailUi,
|
||||||
VitalsVM? Vitals,
|
VitalsVM? Vitals,
|
||||||
ChatVM? RetailChat,
|
|
||||||
CharacterSheetProvider? CharacterSheet,
|
CharacterSheetProvider? CharacterSheet,
|
||||||
MagicRuntime? Magic,
|
MagicRuntime? Magic,
|
||||||
PaperdollFramePresenter? Paperdoll);
|
PaperdollFramePresenter? Paperdoll);
|
||||||
|
|
@ -140,7 +137,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
Selection: new(
|
Selection: new(
|
||||||
SetPlayerIdentity: id => _player.Identity.ServerGuid = id,
|
SetPlayerIdentity: id => _player.Identity.ServerGuid = id,
|
||||||
SetVitalsIdentity: id => _ui.Vitals?.SetLocalPlayerGuid(id),
|
SetVitalsIdentity: id => _ui.Vitals?.SetLocalPlayerGuid(id),
|
||||||
SetChatIdentity: _domain.Chat.SetLocalPlayerGuid,
|
SetChatIdentity: _domain.Communication.Chat.SetLocalPlayerGuid,
|
||||||
MarkPersistent: _world.WorldState.MarkPersistent,
|
MarkPersistent: _world.WorldState.MarkPersistent,
|
||||||
SetVanishProbeIdentity: id => EntityVanishProbe.PlayerGuid = id,
|
SetVanishProbeIdentity: id => EntityVanishProbe.PlayerGuid = id,
|
||||||
ClearCombat: _domain.Combat.Clear),
|
ClearCombat: _domain.Combat.Clear),
|
||||||
|
|
@ -151,11 +148,11 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
LoadCharacterSettings: _interaction.Settings.LoadCharacterContext,
|
LoadCharacterSettings: _interaction.Settings.LoadCharacterContext,
|
||||||
ArmPlayerModeAutoEntry: _interaction.PlayerModeAutoEntry.Arm),
|
ArmPlayerModeAutoEntry: _interaction.PlayerModeAutoEntry.Arm),
|
||||||
Connecting: (host, port, user) =>
|
Connecting: (host, port, user) =>
|
||||||
_domain.Chat.OnSystemMessage(
|
_domain.Communication.Chat.OnSystemMessage(
|
||||||
$"connecting to {host}:{port} as {user}",
|
$"connecting to {host}:{port} as {user}",
|
||||||
chatType: 1),
|
chatType: 1),
|
||||||
Connected: () =>
|
Connected: () =>
|
||||||
_domain.Chat.OnSystemMessage(
|
_domain.Communication.Chat.OnSystemMessage(
|
||||||
"connected — character list received",
|
"connected — character list received",
|
||||||
chatType: 1)),
|
chatType: 1)),
|
||||||
connectOptions);
|
connectOptions);
|
||||||
|
|
@ -167,7 +164,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
PlayerPresentation = ResetPlayerPresentation,
|
PlayerPresentation = ResetPlayerPresentation,
|
||||||
TeleportTransit = _world.Teleport.ResetSession,
|
TeleportTransit = _world.Teleport.ResetSession,
|
||||||
SessionDialogs = () => _ui.RetailUi?.ResetSessionTransientUi(),
|
SessionDialogs = () => _ui.RetailUi?.ResetSessionTransientUi(),
|
||||||
ChatCommandTargets = () => _ui.RetailChat?.ResetSessionTargets(),
|
ChatCommandTargets = _domain.Communication.ResetCommandTargets,
|
||||||
SettingsCharacterContext =
|
SettingsCharacterContext =
|
||||||
_interaction.Settings.RestoreDefaultCharacterContext,
|
_interaction.Settings.RestoreDefaultCharacterContext,
|
||||||
EquippedChildren = _world.EquippedChildren.Clear,
|
EquippedChildren = _world.EquippedChildren.Clear,
|
||||||
|
|
@ -181,9 +178,9 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
CombatState = _domain.Combat.Clear,
|
CombatState = _domain.Combat.Clear,
|
||||||
ItemMana = _domain.ItemMana.Clear,
|
ItemMana = _domain.ItemMana.Clear,
|
||||||
LocalPlayer = _domain.LocalPlayer.Clear,
|
LocalPlayer = _domain.LocalPlayer.Clear,
|
||||||
Friends = _domain.Friends.Clear,
|
Friends = _domain.Communication.ResetFriends,
|
||||||
Squelch = _domain.Squelch.Clear,
|
Squelch = _domain.Communication.ResetSquelch,
|
||||||
TurbineChat = _domain.TurbineChat.Reset,
|
TurbineChat = _domain.Communication.ResetNegotiatedChannels,
|
||||||
ParticleVisibility = _world.ParticleVisibility.Reset,
|
ParticleVisibility = _world.ParticleVisibility.Reset,
|
||||||
InboundEventFifo = _world.InboundEvents.Clear,
|
InboundEventFifo = _world.InboundEvents.Clear,
|
||||||
LiveLiveness = _world.Liveness.Clear,
|
LiveLiveness = _world.Liveness.Clear,
|
||||||
|
|
@ -218,7 +215,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
||||||
_player.Identity.ServerGuid = 0u;
|
_player.Identity.ServerGuid = 0u;
|
||||||
_ui.Vitals?.SetLocalPlayerGuid(0u);
|
_ui.Vitals?.SetLocalPlayerGuid(0u);
|
||||||
_domain.Chat.ResetSessionIdentity();
|
_domain.Communication.ResetChatIdentity();
|
||||||
EntityVanishProbe.PlayerGuid = 0u;
|
EntityVanishProbe.PlayerGuid = 0u;
|
||||||
_interaction.Settings.ResetActiveCharacterKey();
|
_interaction.Settings.ResetActiveCharacterKey();
|
||||||
_player.CharacterOptions.Reset();
|
_player.CharacterOptions.Reset();
|
||||||
|
|
@ -254,10 +251,10 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
CreateInventoryBindings(),
|
CreateInventoryBindings(),
|
||||||
CreateCharacterBindings(skillTable),
|
CreateCharacterBindings(skillTable),
|
||||||
new LiveSocialSessionBindings(
|
new LiveSocialSessionBindings(
|
||||||
_domain.Chat,
|
_domain.Communication.Chat,
|
||||||
_domain.TurbineChat,
|
_domain.Communication.TurbineChat,
|
||||||
_domain.Friends,
|
_domain.Communication.Friends,
|
||||||
_domain.Squelch));
|
_domain.Communication.Squelch));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||||
|
|
@ -331,8 +328,10 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
ToggleUiLock: () =>
|
ToggleUiLock: () =>
|
||||||
_interaction.Settings.SetUiLocked(
|
_interaction.Settings.SetUiLocked(
|
||||||
!_interaction.Settings.Gameplay.LockUI),
|
!_interaction.Settings.Gameplay.LockUI),
|
||||||
ShowSystemMessage: text => _domain.Chat.OnSystemMessage(text, 0u),
|
ShowSystemMessage:
|
||||||
ShowWeenieError: code => _domain.Chat.OnWeenieError(code, null),
|
text => _domain.Communication.Chat.OnSystemMessage(text, 0u),
|
||||||
|
ShowWeenieError:
|
||||||
|
code => _domain.Communication.Chat.OnWeenieError(code, null),
|
||||||
PlayerPublicWeenieBitfield: () =>
|
PlayerPublicWeenieBitfield: () =>
|
||||||
_domain.EntityObjects.Objects.Get(_player.Identity.ServerGuid)?
|
_domain.EntityObjects.Objects.Get(_player.Identity.ServerGuid)?
|
||||||
.PublicWeenieBitfield,
|
.PublicWeenieBitfield,
|
||||||
|
|
@ -346,7 +345,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
ShowConfirmation: (message, completed) =>
|
ShowConfirmation: (message, completed) =>
|
||||||
_ui.RetailUi?.ShowConfirmation(message, completed),
|
_ui.RetailUi?.ShowConfirmation(message, completed),
|
||||||
Suicide: session.SendSuicide,
|
Suicide: session.SendSuicide,
|
||||||
ClearChat: _ => _domain.Chat.Clear(),
|
ClearChat: _ => _domain.Communication.Chat.Clear(),
|
||||||
SaveUi: name => _ui.RetailUi?.SaveNamedLayout(name),
|
SaveUi: name => _ui.RetailUi?.SaveNamedLayout(name),
|
||||||
LoadUi: name => _ui.RetailUi?.RestoreNamedLayout(name),
|
LoadUi: name => _ui.RetailUi?.RestoreNamedLayout(name),
|
||||||
SaveAutoUi: () => _ui.RetailUi?.SaveLayout(),
|
SaveAutoUi: () => _ui.RetailUi?.SaveLayout(),
|
||||||
|
|
@ -363,16 +362,17 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
ClearConsent: session.SendClearConsent,
|
ClearConsent: session.SendClearConsent,
|
||||||
RemoveConsent: session.SendRemoveConsent,
|
RemoveConsent: session.SendRemoveConsent,
|
||||||
SendEmote: session.SendEmote,
|
SendEmote: session.SendEmote,
|
||||||
_domain.Friends,
|
_domain.Communication.Friends,
|
||||||
AddFriend: session.SendAddFriend,
|
AddFriend: session.SendAddFriend,
|
||||||
RemoveFriend: session.SendRemoveFriend,
|
RemoveFriend: session.SendRemoveFriend,
|
||||||
ClearFriends: session.SendClearFriends,
|
ClearFriends: session.SendClearFriends,
|
||||||
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
|
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
|
||||||
_domain.Squelch,
|
_domain.Communication.Squelch,
|
||||||
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
|
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
|
||||||
ModifyAccountSquelch: session.SendModifyAccountSquelch,
|
ModifyAccountSquelch: session.SendModifyAccountSquelch,
|
||||||
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
|
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
|
||||||
LastTeller: () => _ui.RetailChat?.LastIncomingTellSender,
|
LastTeller: () =>
|
||||||
|
_domain.Communication.CommandTargets.LastIncomingTellSender,
|
||||||
ClearDesiredComponents: () =>
|
ClearDesiredComponents: () =>
|
||||||
{
|
{
|
||||||
session.SendClearDesiredComponents();
|
session.SendClearDesiredComponents();
|
||||||
|
|
@ -380,8 +380,8 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
},
|
},
|
||||||
HasOpenVendor: () => false,
|
HasOpenVendor: () => false,
|
||||||
FillComponentBuyList: (_, _) => { }),
|
FillComponentBuyList: (_, _) => { }),
|
||||||
_domain.Chat,
|
_domain.Communication.Chat,
|
||||||
_domain.TurbineChat,
|
_domain.Communication.TurbineChat,
|
||||||
PlayerGuid: () => _player.Identity.ServerGuid,
|
PlayerGuid: () => _player.Identity.ServerGuid,
|
||||||
SendTalk: session.SendTalk,
|
SendTalk: session.SendTalk,
|
||||||
SendTell: session.SendTell,
|
SendTell: session.SendTell,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using AcDream.App.Settings;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using Silk.NET.Input;
|
using Silk.NET.Input;
|
||||||
|
|
@ -323,17 +324,18 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.World.LiveEntityRuntime? _liveEntities;
|
private AcDream.App.World.LiveEntityRuntime? _liveEntities;
|
||||||
private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness;
|
private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness;
|
||||||
|
|
||||||
// Phase F.1-H.1 — client-side state classes fed by GameEventWiring.
|
// J4.1: Runtime owns communication/social state. App, UI, plugins, and
|
||||||
// Exposed publicly so plugins + UI panels can bind directly.
|
// live-session routing borrow these exact instances.
|
||||||
public readonly AcDream.Core.Chat.ChatLog Chat = new();
|
private readonly RuntimeCommunicationState _runtimeCommunication = new();
|
||||||
// Phase I.6 — runtime state for retail's TurbineChat (0xF7DE) global
|
public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat;
|
||||||
// chat rooms. Empty/disabled until the server fires
|
public AcDream.Core.Chat.TurbineChatState TurbineChat =>
|
||||||
// SetTurbineChatChannels (0x0295) shortly after EnterWorld.
|
_runtimeCommunication.TurbineChat;
|
||||||
public readonly AcDream.Core.Chat.TurbineChatState TurbineChat = new();
|
public AcDream.Core.Social.FriendsState Friends =>
|
||||||
|
_runtimeCommunication.Friends;
|
||||||
|
public AcDream.Core.Social.SquelchState Squelch =>
|
||||||
|
_runtimeCommunication.Squelch;
|
||||||
public readonly AcDream.Core.Combat.CombatState Combat = new();
|
public readonly AcDream.Core.Combat.CombatState Combat = new();
|
||||||
public readonly AcDream.Core.Items.ItemManaState ItemMana = new();
|
public readonly AcDream.Core.Items.ItemManaState ItemMana = new();
|
||||||
public readonly AcDream.Core.Social.FriendsState Friends = new();
|
|
||||||
public readonly AcDream.Core.Social.SquelchState Squelch = new();
|
|
||||||
private readonly DesiredComponentSnapshotState _desiredComponents = new();
|
private readonly DesiredComponentSnapshotState _desiredComponents = new();
|
||||||
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
|
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
|
||||||
_desiredComponents.Items;
|
_desiredComponents.Items;
|
||||||
|
|
@ -1208,7 +1210,7 @@ public sealed class GameWindow :
|
||||||
hostInputCamera.CameraController,
|
hostInputCamera.CameraController,
|
||||||
contentEffectsAudio.Audio?.Engine),
|
contentEffectsAudio.Audio?.Engine),
|
||||||
_hostQuiescence,
|
_hostQuiescence,
|
||||||
Chat,
|
_runtimeCommunication,
|
||||||
Combat,
|
Combat,
|
||||||
LocalPlayer,
|
LocalPlayer,
|
||||||
optionalDevTools,
|
optionalDevTools,
|
||||||
|
|
@ -1264,7 +1266,7 @@ public sealed class GameWindow :
|
||||||
_runtimeEntityObjects.Objects,
|
_runtimeEntityObjects.Objects,
|
||||||
contentEffectsAudio.MagicCatalog,
|
contentEffectsAudio.MagicCatalog,
|
||||||
SpellBook,
|
SpellBook,
|
||||||
Chat,
|
_runtimeCommunication,
|
||||||
LocalPlayer,
|
LocalPlayer,
|
||||||
ItemMana,
|
ItemMana,
|
||||||
_stackSplitQuantity,
|
_stackSplitQuantity,
|
||||||
|
|
@ -1409,13 +1411,10 @@ public sealed class GameWindow :
|
||||||
_combatAttackOperations,
|
_combatAttackOperations,
|
||||||
Combat,
|
Combat,
|
||||||
_combatFeedback,
|
_combatFeedback,
|
||||||
Chat,
|
_runtimeCommunication,
|
||||||
LocalPlayer,
|
LocalPlayer,
|
||||||
SpellBook,
|
SpellBook,
|
||||||
ItemMana,
|
ItemMana,
|
||||||
Friends,
|
|
||||||
Squelch,
|
|
||||||
TurbineChat,
|
|
||||||
_externalContainers,
|
_externalContainers,
|
||||||
_portalTunnelFallback,
|
_portalTunnelFallback,
|
||||||
Console.WriteLine),
|
Console.WriteLine),
|
||||||
|
|
@ -1591,6 +1590,7 @@ public sealed class GameWindow :
|
||||||
_equippedChildRenderer,
|
_equippedChildRenderer,
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
_runtimeEntityObjects,
|
_runtimeEntityObjects,
|
||||||
|
_runtimeCommunication,
|
||||||
_renderSceneShadow,
|
_renderSceneShadow,
|
||||||
_livePresentationBindings,
|
_livePresentationBindings,
|
||||||
_entityEffectAdvance,
|
_entityEffectAdvance,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ using AcDream.Content;
|
||||||
using AcDream.Core.Audio;
|
using AcDream.Core.Audio;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
|
|
@ -83,6 +84,7 @@ internal sealed record LiveShutdownRoots(
|
||||||
EquippedChildRenderController? EquippedChildren,
|
EquippedChildRenderController? EquippedChildren,
|
||||||
LiveEntityRuntime? LiveEntities,
|
LiveEntityRuntime? LiveEntities,
|
||||||
RuntimeEntityObjectLifetime EntityObjects,
|
RuntimeEntityObjectLifetime EntityObjects,
|
||||||
|
RuntimeCommunicationState Communication,
|
||||||
RenderSceneShadowRuntime? RenderSceneShadow,
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
LivePresentationRuntimeBindings? PresentationBindings,
|
LivePresentationRuntimeBindings? PresentationBindings,
|
||||||
DeferredEntityEffectAdvanceSource EffectAdvance,
|
DeferredEntityEffectAdvanceSource EffectAdvance,
|
||||||
|
|
@ -439,6 +441,10 @@ internal static class GameWindowShutdownManifest
|
||||||
Hard("sky", () => render.Sky?.Dispose()),
|
Hard("sky", () => render.Sky?.Dispose()),
|
||||||
Hard("particles", () => render.Particles?.Dispose()),
|
Hard("particles", () => render.Particles?.Dispose()),
|
||||||
]),
|
]),
|
||||||
|
new ResourceShutdownStage("runtime communication state",
|
||||||
|
[
|
||||||
|
Hard("communication owner", live.Communication.Dispose),
|
||||||
|
]),
|
||||||
new ResourceShutdownStage("shared texture owners",
|
new ResourceShutdownStage("shared texture owners",
|
||||||
[
|
[
|
||||||
Hard("sampler cache", () => render.Samplers?.Dispose()),
|
Hard("sampler cache", () => render.Samplers?.Dispose()),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using AcDream.Core.Chat;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
LocalPlayerIdentityState playerIdentity,
|
LocalPlayerIdentityState playerIdentity,
|
||||||
LiveEntityRuntime entities,
|
LiveEntityRuntime entities,
|
||||||
RuntimeEntityObjectLifetime entityObjects,
|
RuntimeEntityObjectLifetime entityObjects,
|
||||||
ChatLog chat,
|
RuntimeCommunicationState communication,
|
||||||
ILocalPlayerControllerSource playerController,
|
ILocalPlayerControllerSource playerController,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
IGameRuntimeClock clock,
|
IGameRuntimeClock clock,
|
||||||
|
|
@ -50,7 +51,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
playerIdentity,
|
playerIdentity,
|
||||||
entities,
|
entities,
|
||||||
entityObjects,
|
entityObjects,
|
||||||
chat,
|
communication,
|
||||||
playerController,
|
playerController,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
clock);
|
clock);
|
||||||
|
|
@ -60,7 +61,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
_events = new CurrentGameRuntimeEventAdapter(
|
_events = new CurrentGameRuntimeEventAdapter(
|
||||||
_view,
|
_view,
|
||||||
entityObjects,
|
entityObjects,
|
||||||
chat);
|
communication);
|
||||||
_commands = new CurrentGameRuntimeCommandAdapter(
|
_commands = new CurrentGameRuntimeCommandAdapter(
|
||||||
session,
|
session,
|
||||||
sessionHost,
|
sessionHost,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using AcDream.Core.Chat;
|
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Runtime;
|
namespace AcDream.App.Runtime;
|
||||||
|
|
||||||
|
|
@ -25,14 +25,16 @@ internal sealed class CurrentGameRuntimeEventAdapter
|
||||||
: IRuntimeEventSource,
|
: IRuntimeEventSource,
|
||||||
ICurrentGameRuntimeEventSink,
|
ICurrentGameRuntimeEventSink,
|
||||||
IRuntimeEntityObjectObserver,
|
IRuntimeEntityObjectObserver,
|
||||||
|
IRuntimeCommunicationObserver,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
private readonly CurrentGameRuntimeViewAdapter _view;
|
private readonly CurrentGameRuntimeViewAdapter _view;
|
||||||
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
||||||
private readonly ChatLog _chat;
|
private readonly RuntimeCommunicationState _communication;
|
||||||
private readonly object _observerGate = new();
|
private readonly object _observerGate = new();
|
||||||
private IRuntimeEventObserver[] _observers = [];
|
private IRuntimeEventObserver[] _observers = [];
|
||||||
private IDisposable? _entityObjectSubscription;
|
private IDisposable? _entityObjectSubscription;
|
||||||
|
private IDisposable? _communicationSubscription;
|
||||||
private bool _ownerEventsAttached;
|
private bool _ownerEventsAttached;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -42,12 +44,13 @@ internal sealed class CurrentGameRuntimeEventAdapter
|
||||||
public CurrentGameRuntimeEventAdapter(
|
public CurrentGameRuntimeEventAdapter(
|
||||||
CurrentGameRuntimeViewAdapter view,
|
CurrentGameRuntimeViewAdapter view,
|
||||||
RuntimeEntityObjectLifetime entityObjects,
|
RuntimeEntityObjectLifetime entityObjects,
|
||||||
ChatLog chat)
|
RuntimeCommunicationState communication)
|
||||||
{
|
{
|
||||||
_view = view ?? throw new ArgumentNullException(nameof(view));
|
_view = view ?? throw new ArgumentNullException(nameof(view));
|
||||||
_entityObjects = entityObjects
|
_entityObjects = entityObjects
|
||||||
?? throw new ArgumentNullException(nameof(entityObjects));
|
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||||
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
_communication = communication
|
||||||
|
?? throw new ArgumentNullException(nameof(communication));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable Subscribe(IRuntimeEventObserver observer)
|
public IDisposable Subscribe(IRuntimeEventObserver observer)
|
||||||
|
|
@ -175,7 +178,7 @@ internal sealed class CurrentGameRuntimeEventAdapter
|
||||||
private void AttachOwnerEvents()
|
private void AttachOwnerEvents()
|
||||||
{
|
{
|
||||||
_entityObjectSubscription = _entityObjects.Events.Subscribe(this);
|
_entityObjectSubscription = _entityObjects.Events.Subscribe(this);
|
||||||
_chat.EntryAppended += OnChatEntry;
|
_communicationSubscription = _communication.Events.Subscribe(this);
|
||||||
_ownerEventsAttached = true;
|
_ownerEventsAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +186,8 @@ internal sealed class CurrentGameRuntimeEventAdapter
|
||||||
{
|
{
|
||||||
if (!_ownerEventsAttached)
|
if (!_ownerEventsAttached)
|
||||||
return;
|
return;
|
||||||
_chat.EntryAppended -= OnChatEntry;
|
_communicationSubscription?.Dispose();
|
||||||
|
_communicationSubscription = null;
|
||||||
_entityObjectSubscription?.Dispose();
|
_entityObjectSubscription?.Dispose();
|
||||||
_entityObjectSubscription = null;
|
_entityObjectSubscription = null;
|
||||||
_ownerEventsAttached = false;
|
_ownerEventsAttached = false;
|
||||||
|
|
@ -223,19 +227,13 @@ internal sealed class CurrentGameRuntimeEventAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChatEntry(ChatEntry entry)
|
public void OnChat(in RuntimeCommunicationEvent committed)
|
||||||
{
|
{
|
||||||
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||||
return;
|
return;
|
||||||
var delta = new RuntimeChatDelta(
|
var delta = new RuntimeChatDelta(
|
||||||
NextStamp(),
|
NextStamp(),
|
||||||
new RuntimeChatEntry(
|
committed.Entry);
|
||||||
_chat.Revision,
|
|
||||||
entry.SenderGuid,
|
|
||||||
(int)entry.Kind,
|
|
||||||
entry.Sender,
|
|
||||||
entry.Text,
|
|
||||||
entry.ChannelName));
|
|
||||||
foreach (IRuntimeEventObserver observer in observers)
|
foreach (IRuntimeEventObserver observer in observers)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Chat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
namespace AcDream.App.Runtime;
|
namespace AcDream.App.Runtime;
|
||||||
|
|
@ -20,11 +21,10 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
private readonly LocalPlayerIdentityState _playerIdentity;
|
private readonly LocalPlayerIdentityState _playerIdentity;
|
||||||
private readonly LiveEntityRuntime _entities;
|
private readonly LiveEntityRuntime _entities;
|
||||||
private readonly ClientObjectTable _objects;
|
private readonly ClientObjectTable _objects;
|
||||||
private readonly ChatLog _chat;
|
|
||||||
private readonly IGameRuntimeClock _clock;
|
private readonly IGameRuntimeClock _clock;
|
||||||
private readonly IRuntimeEntityView _entityView;
|
private readonly IRuntimeEntityView _entityView;
|
||||||
private readonly IRuntimeInventoryView _inventoryView;
|
private readonly IRuntimeInventoryView _inventoryView;
|
||||||
private readonly ChatView _chatView;
|
private readonly IRuntimeChatView _chatView;
|
||||||
private readonly MovementView _movementView;
|
private readonly MovementView _movementView;
|
||||||
private readonly PortalView _portalView;
|
private readonly PortalView _portalView;
|
||||||
private bool _active = true;
|
private bool _active = true;
|
||||||
|
|
@ -34,7 +34,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
LocalPlayerIdentityState playerIdentity,
|
LocalPlayerIdentityState playerIdentity,
|
||||||
LiveEntityRuntime entities,
|
LiveEntityRuntime entities,
|
||||||
RuntimeEntityObjectLifetime entityObjects,
|
RuntimeEntityObjectLifetime entityObjects,
|
||||||
ChatLog chat,
|
RuntimeCommunicationState communication,
|
||||||
ILocalPlayerControllerSource playerController,
|
ILocalPlayerControllerSource playerController,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
IGameRuntimeClock clock)
|
IGameRuntimeClock clock)
|
||||||
|
|
@ -45,12 +45,12 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||||
ArgumentNullException.ThrowIfNull(entityObjects);
|
ArgumentNullException.ThrowIfNull(entityObjects);
|
||||||
_objects = entityObjects.Objects;
|
_objects = entityObjects.Objects;
|
||||||
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
ArgumentNullException.ThrowIfNull(communication);
|
||||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||||
|
|
||||||
_entityView = entityObjects.EntityView;
|
_entityView = entityObjects.EntityView;
|
||||||
_inventoryView = entityObjects.InventoryView;
|
_inventoryView = entityObjects.InventoryView;
|
||||||
_chatView = new ChatView(_chat);
|
_chatView = communication.View;
|
||||||
_movementView = new MovementView(
|
_movementView = new MovementView(
|
||||||
playerController
|
playerController
|
||||||
?? throw new ArgumentNullException(nameof(playerController)),
|
?? throw new ArgumentNullException(nameof(playerController)),
|
||||||
|
|
@ -103,19 +103,13 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
_entities.MaterializedCount,
|
_entities.MaterializedCount,
|
||||||
_objects.ObjectCount,
|
_objects.ObjectCount,
|
||||||
_objects.ContainerCount,
|
_objects.ContainerCount,
|
||||||
_chat.Revision,
|
_chatView.Revision,
|
||||||
_chat.Count,
|
_chatView.Count,
|
||||||
_movementView.Snapshot,
|
_movementView.Snapshot,
|
||||||
_portalView.Snapshot);
|
_portalView.Snapshot);
|
||||||
|
|
||||||
internal void Deactivate() => _active = false;
|
internal void Deactivate() => _active = false;
|
||||||
|
|
||||||
private sealed class ChatView(ChatLog owner) : IRuntimeChatView
|
|
||||||
{
|
|
||||||
public long Revision => owner.Revision;
|
|
||||||
public int Count => owner.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MovementView(
|
private sealed class MovementView(
|
||||||
ILocalPlayerControllerSource owner,
|
ILocalPlayerControllerSource owner,
|
||||||
IGameRuntimeClock clock)
|
IGameRuntimeClock clock)
|
||||||
|
|
|
||||||
84
src/AcDream.Core/Chat/ChatCommandTargetState.cs
Normal file
84
src/AcDream.Core/Chat/ChatCommandTargetState.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
namespace AcDream.Core.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Presentation-independent reply/retell targets derived from the committed
|
||||||
|
/// chat transcript.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Incoming tells carry a non-zero sender GUID and update the reply target.
|
||||||
|
/// Local outgoing tell echoes carry a zero sender GUID and place the target
|
||||||
|
/// name in <see cref="ChatEntry.Sender"/>. This is the existing retail
|
||||||
|
/// <c>@reply</c>/<c>@retell</c> behavior formerly owned by <c>ChatVM</c>.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class ChatCommandTargetState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ChatLog _chat;
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private string? _lastIncomingTellSender;
|
||||||
|
private string? _lastOutgoingTellTarget;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public ChatCommandTargetState(ChatLog chat)
|
||||||
|
{
|
||||||
|
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||||
|
_chat.EntryAppended += OnEntryAppended;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? LastIncomingTellSender
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
return _lastIncomingTellSender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? LastOutgoingTellTarget
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
return _lastOutgoingTellTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forget character-scoped command targets without clearing visible
|
||||||
|
/// transcript history.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_lastIncomingTellSender = null;
|
||||||
|
_lastOutgoingTellTarget = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
_chat.EntryAppended -= OnEntryAppended;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntryAppended(ChatEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Kind != ChatKind.Tell || string.IsNullOrEmpty(entry.Sender))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
if (entry.SenderGuid != 0u)
|
||||||
|
_lastIncomingTellSender = entry.Sender;
|
||||||
|
else
|
||||||
|
_lastOutgoingTellTarget = entry.Sender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
263
src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs
Normal file
263
src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Core.Social;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCommunicationEvent(
|
||||||
|
ulong Sequence,
|
||||||
|
RuntimeChatEntry Entry);
|
||||||
|
|
||||||
|
public interface IRuntimeCommunicationObserver
|
||||||
|
{
|
||||||
|
void OnChat(in RuntimeCommunicationEvent delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRuntimeCommunicationEventSource
|
||||||
|
{
|
||||||
|
IDisposable Subscribe(IRuntimeCommunicationObserver observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical presentation-independent owner for communication and social
|
||||||
|
/// state. Graphical, headless, plugin, and bot hosts borrow these exact
|
||||||
|
/// instances.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RuntimeCommunicationState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly RuntimeCommunicationEventStream _events;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public RuntimeCommunicationState(int maximumChatEntries = 500)
|
||||||
|
{
|
||||||
|
Chat = new ChatLog(maximumChatEntries);
|
||||||
|
CommandTargets = new ChatCommandTargetState(Chat);
|
||||||
|
_events = new RuntimeCommunicationEventStream(Chat);
|
||||||
|
TurbineChat = new TurbineChatState();
|
||||||
|
Friends = new FriendsState();
|
||||||
|
Squelch = new SquelchState();
|
||||||
|
View = new CommunicationView(Chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatLog Chat { get; }
|
||||||
|
public ChatCommandTargetState CommandTargets { get; }
|
||||||
|
public TurbineChatState TurbineChat { get; }
|
||||||
|
public FriendsState Friends { get; }
|
||||||
|
public SquelchState Squelch { get; }
|
||||||
|
public IRuntimeChatView View { get; }
|
||||||
|
public IRuntimeCommunicationEventSource Events => _events;
|
||||||
|
|
||||||
|
public bool IsDisposed => _disposed;
|
||||||
|
public ulong LastSequence => _events.LastSequence;
|
||||||
|
public int SubscriberCount => _events.SubscriberCount;
|
||||||
|
public int PendingDispatchCount => _events.PendingDispatchCount;
|
||||||
|
public bool IsDispatching => _events.IsDispatching;
|
||||||
|
public long DispatchFailureCount => _events.DispatchFailureCount;
|
||||||
|
public Exception? LastDispatchFailure => _events.LastDispatchFailure;
|
||||||
|
|
||||||
|
public void ResetCommandTargets() => CommandTargets.ResetSession();
|
||||||
|
public void ResetChatIdentity() => Chat.ResetSessionIdentity();
|
||||||
|
public void ResetNegotiatedChannels() => TurbineChat.Reset();
|
||||||
|
public void ResetFriends() => Friends.Clear();
|
||||||
|
public void ResetSquelch() => Squelch.Clear();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
_events.Dispose();
|
||||||
|
CommandTargets.ResetSession();
|
||||||
|
CommandTargets.Dispose();
|
||||||
|
TurbineChat.Reset();
|
||||||
|
Friends.Clear();
|
||||||
|
Squelch.Clear();
|
||||||
|
Chat.ResetSessionIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CommunicationView(ChatLog chat) : IRuntimeChatView
|
||||||
|
{
|
||||||
|
public long Revision => chat.Revision;
|
||||||
|
public int Count => chat.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RuntimeCommunicationEventStream
|
||||||
|
: IRuntimeCommunicationEventSource,
|
||||||
|
IDisposable
|
||||||
|
{
|
||||||
|
private readonly ChatLog _chat;
|
||||||
|
private readonly object _gate = new();
|
||||||
|
private readonly List<RuntimeCommunicationEvent> _pendingDispatch = [];
|
||||||
|
private IRuntimeCommunicationObserver[] _observers = [];
|
||||||
|
private long _sequence;
|
||||||
|
private long _dispatchFailureCount;
|
||||||
|
private bool _dispatching;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public RuntimeCommunicationEventStream(ChatLog chat)
|
||||||
|
{
|
||||||
|
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||||
|
_chat.EntryAppended += OnEntryAppended;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SubscriberCount => Volatile.Read(ref _observers).Length;
|
||||||
|
public ulong LastSequence
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
return unchecked((ulong)_sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public int PendingDispatchCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
return _pendingDispatch.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool IsDispatching
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
return _dispatching;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public long DispatchFailureCount => Interlocked.Read(ref _dispatchFailureCount);
|
||||||
|
public Exception? LastDispatchFailure { get; private set; }
|
||||||
|
|
||||||
|
public IDisposable Subscribe(IRuntimeCommunicationObserver observer)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(observer);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
IRuntimeCommunicationObserver[] current = _observers;
|
||||||
|
if (Array.IndexOf(current, observer) >= 0)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The communication observer is already subscribed.");
|
||||||
|
|
||||||
|
var replacement =
|
||||||
|
new IRuntimeCommunicationObserver[current.Length + 1];
|
||||||
|
Array.Copy(current, replacement, current.Length);
|
||||||
|
replacement[^1] = observer;
|
||||||
|
Volatile.Write(ref _observers, replacement);
|
||||||
|
}
|
||||||
|
return new Subscription(this, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
_chat.EntryAppended -= OnEntryAppended;
|
||||||
|
_pendingDispatch.Clear();
|
||||||
|
_dispatching = false;
|
||||||
|
Volatile.Write(ref _observers, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEntryAppended(ChatEntry entry)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
ulong sequence = unchecked((ulong)++_sequence);
|
||||||
|
_pendingDispatch.Add(new RuntimeCommunicationEvent(
|
||||||
|
sequence,
|
||||||
|
new RuntimeChatEntry(
|
||||||
|
_chat.Revision,
|
||||||
|
entry.SenderGuid,
|
||||||
|
(int)entry.Kind,
|
||||||
|
entry.Sender,
|
||||||
|
entry.Text,
|
||||||
|
entry.ChannelName)));
|
||||||
|
if (_dispatching)
|
||||||
|
return;
|
||||||
|
_dispatching = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
RuntimeCommunicationEvent pending;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (index >= _pendingDispatch.Count)
|
||||||
|
{
|
||||||
|
_pendingDispatch.Clear();
|
||||||
|
_dispatching = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pending = _pendingDispatch[index++];
|
||||||
|
}
|
||||||
|
Dispatch(in pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dispatch(in RuntimeCommunicationEvent delta)
|
||||||
|
{
|
||||||
|
IRuntimeCommunicationObserver[] observers =
|
||||||
|
Volatile.Read(ref _observers);
|
||||||
|
foreach (IRuntimeCommunicationObserver observer in observers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
observer.OnChat(in delta);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _dispatchFailureCount);
|
||||||
|
LastDispatchFailure = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unsubscribe(IRuntimeCommunicationObserver observer)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
IRuntimeCommunicationObserver[] current = _observers;
|
||||||
|
int index = Array.IndexOf(current, observer);
|
||||||
|
if (index < 0)
|
||||||
|
return;
|
||||||
|
if (current.Length == 1)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _observers, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var replacement =
|
||||||
|
new IRuntimeCommunicationObserver[current.Length - 1];
|
||||||
|
if (index > 0)
|
||||||
|
Array.Copy(current, 0, replacement, 0, index);
|
||||||
|
if (index < current.Length - 1)
|
||||||
|
{
|
||||||
|
Array.Copy(
|
||||||
|
current,
|
||||||
|
index + 1,
|
||||||
|
replacement,
|
||||||
|
index,
|
||||||
|
current.Length - index - 1);
|
||||||
|
}
|
||||||
|
Volatile.Write(ref _observers, replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Subscription(
|
||||||
|
RuntimeCommunicationEventStream owner,
|
||||||
|
IRuntimeCommunicationObserver observer)
|
||||||
|
: IDisposable
|
||||||
|
{
|
||||||
|
private RuntimeCommunicationEventStream? _owner = owner;
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unsubscribe(observer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,8 @@ public sealed class ChatVM : IDisposable
|
||||||
public const int DefaultDisplayLimit = 20;
|
public const int DefaultDisplayLimit = 20;
|
||||||
|
|
||||||
private readonly ChatLog _log;
|
private readonly ChatLog _log;
|
||||||
|
private readonly ChatCommandTargetState _commandTargets;
|
||||||
|
private readonly bool _ownsCommandTargets;
|
||||||
private readonly int _displayLimit;
|
private readonly int _displayLimit;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -41,7 +43,8 @@ public sealed class ChatVM : IDisposable
|
||||||
/// <c>chat.rs::ChatState::last_incoming_tell_sender</c> (line 74 +
|
/// <c>chat.rs::ChatState::last_incoming_tell_sender</c> (line 74 +
|
||||||
/// the assignment at line 152).
|
/// the assignment at line 152).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? LastIncomingTellSender { get; private set; }
|
public string? LastIncomingTellSender =>
|
||||||
|
_commandTargets.LastIncomingTellSender;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Target of the most recent OUTGOING Tell (the player's own
|
/// Target of the most recent OUTGOING Tell (the player's own
|
||||||
|
|
@ -52,7 +55,8 @@ public sealed class ChatVM : IDisposable
|
||||||
/// <c>SenderGuid == 0</c> and the target name in <c>Sender</c> —
|
/// <c>SenderGuid == 0</c> and the target name in <c>Sender</c> —
|
||||||
/// that's the discriminator we capture here.
|
/// that's the discriminator we capture here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? LastOutgoingTellTarget { get; private set; }
|
public string? LastOutgoingTellTarget =>
|
||||||
|
_commandTargets.LastOutgoingTellTarget;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional callback exposing the live framerate. Wired by
|
/// Optional callback exposing the live framerate. Wired by
|
||||||
|
|
@ -81,35 +85,25 @@ public sealed class ChatVM : IDisposable
|
||||||
/// <see cref="RecentLines"/> call. Must be >= 1. Defaults to
|
/// <see cref="RecentLines"/> call. Must be >= 1. Defaults to
|
||||||
/// <see cref="DefaultDisplayLimit"/>.
|
/// <see cref="DefaultDisplayLimit"/>.
|
||||||
/// </param>
|
/// </param>
|
||||||
public ChatVM(ChatLog log, int displayLimit = DefaultDisplayLimit)
|
public ChatVM(
|
||||||
|
ChatLog log,
|
||||||
|
int displayLimit = DefaultDisplayLimit,
|
||||||
|
ChatCommandTargetState? commandTargets = null)
|
||||||
{
|
{
|
||||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||||
if (displayLimit < 1)
|
if (displayLimit < 1)
|
||||||
throw new ArgumentOutOfRangeException(nameof(displayLimit), displayLimit, "must be >= 1");
|
throw new ArgumentOutOfRangeException(nameof(displayLimit), displayLimit, "must be >= 1");
|
||||||
_displayLimit = displayLimit;
|
_displayLimit = displayLimit;
|
||||||
_log.EntryAppended += OnEntryAppended;
|
_commandTargets = commandTargets ?? new ChatCommandTargetState(_log);
|
||||||
}
|
_ownsCommandTargets = commandTargets is null;
|
||||||
|
|
||||||
private void OnEntryAppended(ChatEntry entry)
|
|
||||||
{
|
|
||||||
// Tell-tracking discriminator (SenderGuid):
|
|
||||||
// != 0 = real incoming whisper; capture sender for /reply.
|
|
||||||
// == 0 = our own outgoing echo via OnSelfSent; capture the
|
|
||||||
// target (Sender field) for /retell.
|
|
||||||
if (entry.Kind != ChatKind.Tell) return;
|
|
||||||
if (string.IsNullOrEmpty(entry.Sender)) return;
|
|
||||||
|
|
||||||
if (entry.SenderGuid != 0)
|
|
||||||
LastIncomingTellSender = entry.Sender;
|
|
||||||
else
|
|
||||||
LastOutgoingTellTarget = entry.Sender;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_log.EntryAppended -= OnEntryAppended;
|
if (_ownsCommandTargets)
|
||||||
|
_commandTargets.Dispose();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,8 +126,7 @@ public sealed class ChatVM : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResetSessionTargets()
|
public void ResetSessionTargets()
|
||||||
{
|
{
|
||||||
LastIncomingTellSender = null;
|
_commandTargets.ResetSession();
|
||||||
LastOutgoingTellTarget = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
Objects: null!,
|
Objects: null!,
|
||||||
MagicCatalog: null!,
|
MagicCatalog: null!,
|
||||||
Spellbook: null!,
|
Spellbook: null!,
|
||||||
Chat: null!,
|
Communication: null!,
|
||||||
LocalPlayer: null!,
|
LocalPlayer: null!,
|
||||||
ItemMana: null!,
|
ItemMana: null!,
|
||||||
StackSplitQuantity: null!,
|
StackSplitQuantity: null!,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using AcDream.App.Rendering;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Core.Player;
|
using AcDream.Core.Player;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
|
@ -228,7 +229,7 @@ public sealed class SettingsDevToolsCompositionTests
|
||||||
Settings,
|
Settings,
|
||||||
Startup,
|
Startup,
|
||||||
new HostQuiescenceGate(),
|
new HostQuiescenceGate(),
|
||||||
new ChatLog(),
|
new RuntimeCommunicationState(),
|
||||||
new CombatState(),
|
new CombatState(),
|
||||||
new LocalPlayerState(new Spellbook()),
|
new LocalPlayerState(new Spellbook()),
|
||||||
enabled
|
enabled
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"new ResourceShutdownStage(\"live entity dependents\"",
|
"new ResourceShutdownStage(\"live entity dependents\"",
|
||||||
"new ResourceShutdownStage(\"submitted GPU work\"",
|
"new ResourceShutdownStage(\"submitted GPU work\"",
|
||||||
"new ResourceShutdownStage(\"render frontends\"",
|
"new ResourceShutdownStage(\"render frontends\"",
|
||||||
|
"new ResourceShutdownStage(\"runtime communication state\"",
|
||||||
"new ResourceShutdownStage(\"shared texture owners\"",
|
"new ResourceShutdownStage(\"shared texture owners\"",
|
||||||
"new ResourceShutdownStage(\"mesh adapter\"",
|
"new ResourceShutdownStage(\"mesh adapter\"",
|
||||||
"new ResourceShutdownStage(\"remaining render owners\"",
|
"new ResourceShutdownStage(\"remaining render owners\"",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ using AcDream.Core.Selection;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
|
@ -411,7 +412,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
new NoopEntityResources(),
|
new NoopEntityResources(),
|
||||||
EntityObjects);
|
EntityObjects);
|
||||||
Objects = EntityObjects.Objects;
|
Objects = EntityObjects.Objects;
|
||||||
Chat = new ChatLog();
|
Communication = new RuntimeCommunicationState();
|
||||||
Selection = new SelectionState();
|
Selection = new SelectionState();
|
||||||
MovementInput = new DispatcherMovementInputSource();
|
MovementInput = new DispatcherMovementInputSource();
|
||||||
GameplayInput = new GameplayInputFrameController(
|
GameplayInput = new GameplayInputFrameController(
|
||||||
|
|
@ -456,7 +457,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Identity,
|
Identity,
|
||||||
Entities,
|
Entities,
|
||||||
EntityObjects,
|
EntityObjects,
|
||||||
Chat,
|
Communication,
|
||||||
new LocalPlayerControllerSlot(),
|
new LocalPlayerControllerSlot(),
|
||||||
WorldReveal,
|
WorldReveal,
|
||||||
Clock,
|
Clock,
|
||||||
|
|
@ -471,7 +472,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||||
public LiveEntityRuntime Entities { get; }
|
public LiveEntityRuntime Entities { get; }
|
||||||
public ClientObjectTable Objects { get; }
|
public ClientObjectTable Objects { get; }
|
||||||
public ChatLog Chat { get; }
|
public RuntimeCommunicationState Communication { get; }
|
||||||
|
public ChatLog Chat => Communication.Chat;
|
||||||
public SelectionState Selection { get; }
|
public SelectionState Selection { get; }
|
||||||
public DispatcherMovementInputSource MovementInput { get; }
|
public DispatcherMovementInputSource MovementInput { get; }
|
||||||
public GameplayInputFrameController GameplayInput { get; }
|
public GameplayInputFrameController GameplayInput { get; }
|
||||||
|
|
@ -485,6 +487,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Runtime.Dispose();
|
Runtime.Dispose();
|
||||||
|
Communication.Dispose();
|
||||||
_session.Dispose();
|
_session.Dispose();
|
||||||
_items.Dispose();
|
_items.Dispose();
|
||||||
Entities.Clear();
|
Entities.Clear();
|
||||||
|
|
|
||||||
48
tests/AcDream.Core.Tests/Chat/ChatCommandTargetStateTests.cs
Normal file
48
tests/AcDream.Core.Tests/Chat/ChatCommandTargetStateTests.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Chat;
|
||||||
|
|
||||||
|
public sealed class ChatCommandTargetStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void TracksReplyAndRetellTargetsFromCommittedTranscript()
|
||||||
|
{
|
||||||
|
var chat = new ChatLog();
|
||||||
|
using var targets = new ChatCommandTargetState(chat);
|
||||||
|
|
||||||
|
chat.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
||||||
|
chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith");
|
||||||
|
|
||||||
|
Assert.Equal("Bestie", targets.LastIncomingTellSender);
|
||||||
|
Assert.Equal("Caith", targets.LastOutgoingTellTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetSessionForgetsTargetsButPreservesTranscript()
|
||||||
|
{
|
||||||
|
var chat = new ChatLog();
|
||||||
|
using var targets = new ChatCommandTargetState(chat);
|
||||||
|
chat.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
||||||
|
chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith");
|
||||||
|
|
||||||
|
targets.ResetSession();
|
||||||
|
|
||||||
|
Assert.Null(targets.LastIncomingTellSender);
|
||||||
|
Assert.Null(targets.LastOutgoingTellTarget);
|
||||||
|
Assert.Equal(2, chat.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposeDetachesAndIsIdempotent()
|
||||||
|
{
|
||||||
|
var chat = new ChatLog();
|
||||||
|
var targets = new ChatCommandTargetState(chat);
|
||||||
|
|
||||||
|
targets.Dispose();
|
||||||
|
targets.Dispose();
|
||||||
|
chat.OnTellReceived("After", "ignored", 0x50000002u);
|
||||||
|
|
||||||
|
Assert.Null(targets.LastIncomingTellSender);
|
||||||
|
Assert.Null(targets.LastOutgoingTellTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Core.Social;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
|
public sealed class RuntimeCommunicationStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void OwnsOneExactCommunicationGraphAndPublishesCommittedEntries()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCommunicationState();
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = state.Events.Subscribe(observer);
|
||||||
|
|
||||||
|
state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u);
|
||||||
|
|
||||||
|
RuntimeCommunicationEvent delta = Assert.Single(observer.Events);
|
||||||
|
Assert.Equal(1UL, delta.Sequence);
|
||||||
|
Assert.Equal(state.Chat.Revision, delta.Entry.Revision);
|
||||||
|
Assert.Equal(1, state.View.Count);
|
||||||
|
Assert.Equal(state.Chat.Revision, state.View.Revision);
|
||||||
|
Assert.Equal(1UL, state.LastSequence);
|
||||||
|
Assert.Equal(0, state.PendingDispatchCount);
|
||||||
|
Assert.False(state.IsDispatching);
|
||||||
|
Assert.Equal("Bestie", state.CommandTargets.LastIncomingTellSender);
|
||||||
|
Assert.Equal("hello", delta.Entry.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantAppendPreservesSequenceForEveryObserver()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCommunicationState();
|
||||||
|
var reentrant = new ReentrantObserver(state.Chat);
|
||||||
|
var trailing = new RecordingObserver();
|
||||||
|
using IDisposable first = state.Events.Subscribe(reentrant);
|
||||||
|
using IDisposable second = state.Events.Subscribe(trailing);
|
||||||
|
|
||||||
|
state.Chat.OnSystemMessage("first", 0u);
|
||||||
|
|
||||||
|
Assert.Equal([1UL, 2UL], reentrant.Sequences);
|
||||||
|
Assert.Equal(
|
||||||
|
[1UL, 2UL],
|
||||||
|
trailing.Events.Select(item => item.Sequence).ToArray());
|
||||||
|
Assert.Equal(["first", "second"], trailing.Events
|
||||||
|
.Select(item => item.Entry.Text)
|
||||||
|
.ToArray());
|
||||||
|
Assert.Equal(0, state.PendingDispatchCount);
|
||||||
|
Assert.False(state.IsDispatching);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObserverFailureDoesNotStarveLaterObserverOrOwner()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCommunicationState();
|
||||||
|
var recording = new RecordingObserver();
|
||||||
|
using IDisposable throwing =
|
||||||
|
state.Events.Subscribe(new ThrowingObserver());
|
||||||
|
using IDisposable trailing = state.Events.Subscribe(recording);
|
||||||
|
|
||||||
|
state.Chat.OnSystemMessage("still committed", 0u);
|
||||||
|
|
||||||
|
Assert.Equal(1, state.Chat.Count);
|
||||||
|
Assert.Single(recording.Events);
|
||||||
|
Assert.Equal(1, state.DispatchFailureCount);
|
||||||
|
Assert.IsType<InvalidOperationException>(state.LastDispatchFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SessionResetsClearScopedStateWithoutClearingTranscript()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCommunicationState();
|
||||||
|
state.Chat.OnTellReceived("Bestie", "hello", 0x50000001u);
|
||||||
|
state.Chat.OnSelfSent(ChatKind.Tell, "outgoing", "Caith");
|
||||||
|
state.TurbineChat.OnChannelsReceived(
|
||||||
|
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
||||||
|
state.Friends.Apply(new FriendsUpdate(
|
||||||
|
FriendsUpdateType.Full,
|
||||||
|
[new FriendEntry(1u, "Friend", true, false, [], [])]));
|
||||||
|
state.Squelch.Replace(new SquelchDatabase(
|
||||||
|
new Dictionary<string, uint> { ["account"] = 1u },
|
||||||
|
new Dictionary<uint, SquelchInfo>(),
|
||||||
|
new SquelchInfo(string.Empty, false, new HashSet<uint>())));
|
||||||
|
|
||||||
|
state.ResetCommandTargets();
|
||||||
|
state.ResetChatIdentity();
|
||||||
|
state.ResetNegotiatedChannels();
|
||||||
|
state.ResetFriends();
|
||||||
|
state.ResetSquelch();
|
||||||
|
|
||||||
|
Assert.Equal(2, state.Chat.Count);
|
||||||
|
Assert.Null(state.CommandTargets.LastIncomingTellSender);
|
||||||
|
Assert.Null(state.CommandTargets.LastOutgoingTellTarget);
|
||||||
|
Assert.False(state.TurbineChat.Enabled);
|
||||||
|
Assert.Empty(state.Friends.Snapshot());
|
||||||
|
Assert.Empty(state.Squelch.Snapshot().Accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposeDetachesStreamAndAllBorrowedState()
|
||||||
|
{
|
||||||
|
var state = new RuntimeCommunicationState();
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
IDisposable subscription = state.Events.Subscribe(observer);
|
||||||
|
|
||||||
|
state.Dispose();
|
||||||
|
state.Dispose();
|
||||||
|
state.Chat.OnSystemMessage("after dispose", 0u);
|
||||||
|
|
||||||
|
Assert.True(state.IsDisposed);
|
||||||
|
Assert.Equal(0, state.SubscriberCount);
|
||||||
|
Assert.Empty(observer.Events);
|
||||||
|
Assert.Throws<ObjectDisposedException>(
|
||||||
|
() => state.Events.Subscribe(new RecordingObserver()));
|
||||||
|
subscription.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndependentRuntimeInstancesNeverShareState()
|
||||||
|
{
|
||||||
|
using var first = new RuntimeCommunicationState();
|
||||||
|
using var second = new RuntimeCommunicationState();
|
||||||
|
|
||||||
|
first.Chat.OnTellReceived("OnlyFirst", "hello", 0x50000001u);
|
||||||
|
|
||||||
|
Assert.Equal(1, first.View.Count);
|
||||||
|
Assert.Equal(0, second.View.Count);
|
||||||
|
Assert.Equal("OnlyFirst", first.CommandTargets.LastIncomingTellSender);
|
||||||
|
Assert.Null(second.CommandTargets.LastIncomingTellSender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingObserver : IRuntimeCommunicationObserver
|
||||||
|
{
|
||||||
|
public List<RuntimeCommunicationEvent> Events { get; } = [];
|
||||||
|
|
||||||
|
public void OnChat(in RuntimeCommunicationEvent delta) =>
|
||||||
|
Events.Add(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ReentrantObserver(ChatLog chat)
|
||||||
|
: IRuntimeCommunicationObserver
|
||||||
|
{
|
||||||
|
public List<ulong> Sequences { get; } = [];
|
||||||
|
|
||||||
|
public void OnChat(in RuntimeCommunicationEvent delta)
|
||||||
|
{
|
||||||
|
Sequences.Add(delta.Sequence);
|
||||||
|
if (delta.Sequence == 1UL)
|
||||||
|
chat.OnSystemMessage("second", 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ThrowingObserver : IRuntimeCommunicationObserver
|
||||||
|
{
|
||||||
|
public void OnChat(in RuntimeCommunicationEvent delta) =>
|
||||||
|
throw new InvalidOperationException("observer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,20 @@ namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ChatVMRetellAndProvidersTests
|
public sealed class ChatVMRetellAndProvidersTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BorrowedCommandTargetsRemainLiveAfterOneViewModelDisposes()
|
||||||
|
{
|
||||||
|
var log = new ChatLog();
|
||||||
|
using var targets = new ChatCommandTargetState(log);
|
||||||
|
var first = new ChatVM(log, commandTargets: targets);
|
||||||
|
using var second = new ChatVM(log, commandTargets: targets);
|
||||||
|
|
||||||
|
first.Dispose();
|
||||||
|
log.OnTellReceived("Bestie", "incoming", 0x50000001u);
|
||||||
|
|
||||||
|
Assert.Equal("Bestie", second.LastIncomingTellSender);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResetSessionTargets_ClearsReplyAndRetellWithoutClearingTranscript()
|
public void ResetSessionTargets_ClearsReplyAndRetellWithoutClearingTranscript()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue