341 lines
14 KiB
C#
341 lines
14 KiB
C#
using AcDream.Core.Chat;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Player;
|
|
using AcDream.Core.Social;
|
|
using AcDream.Core.Spells;
|
|
|
|
namespace AcDream.App.Net;
|
|
|
|
internal sealed record LiveEntitySessionSink(
|
|
Action<WorldSession.EntitySpawn> Spawned,
|
|
Action<DeleteObject.Parsed> Deleted,
|
|
Action<PickupEvent.Parsed> PickedUp,
|
|
Action<WorldSession.EntityMotionUpdate> MotionUpdated,
|
|
Action<WorldSession.EntityPositionUpdate> PositionUpdated,
|
|
Action<VectorUpdate.Parsed> VectorUpdated,
|
|
Action<SetState.Parsed> StateUpdated,
|
|
Action<ParentEvent.Parsed> ParentUpdated,
|
|
Action<uint> TeleportStarted,
|
|
Action<ObjDescEvent.Parsed> AppearanceUpdated,
|
|
Action<PlayPhysicsScript> PlayPhysicsScript,
|
|
Action<PlayPhysicsScriptType> PlayPhysicsScriptType);
|
|
|
|
internal sealed record LiveEnvironmentSessionSink(
|
|
Action<uint> EnvironChanged,
|
|
Action<double> ServerTimeUpdated);
|
|
|
|
internal sealed record LiveInventorySessionBindings(
|
|
ClientObjectTable Objects,
|
|
LocalPlayerState LocalPlayer,
|
|
Func<uint> PlayerGuid,
|
|
Action<IReadOnlyList<ShortcutEntry>>? OnShortcuts,
|
|
Action<uint>? OnUseDone,
|
|
ItemManaState? ItemMana,
|
|
Action<IReadOnlyList<(uint Id, uint Amount)>>? OnDesiredComponents,
|
|
ExternalContainerState? ExternalContainers);
|
|
|
|
internal sealed record LiveCharacterSessionBindings(
|
|
CombatState Combat,
|
|
Spellbook Spellbook,
|
|
Func<uint, IReadOnlyDictionary<uint, uint>, uint>? ResolveSkillFormulaBonus,
|
|
Action<int, int>? OnSkillsUpdated,
|
|
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
|
Action<GameEvents.CharacterConfirmationDone>? OnConfirmationDone,
|
|
Action<uint, uint>? OnCharacterOptions,
|
|
Func<double>? ClientTime);
|
|
|
|
internal sealed record LiveSocialSessionBindings(
|
|
ChatLog Chat,
|
|
TurbineChatState TurbineChat,
|
|
FriendsState? Friends,
|
|
SquelchState? Squelch);
|
|
|
|
/// <summary>
|
|
/// Owns every inbound subscription for one exact live session. Domain state
|
|
/// remains in the supplied sinks; this class owns only routing and teardown.
|
|
/// </summary>
|
|
internal sealed class LiveSessionEventRouter : IDisposable
|
|
{
|
|
private readonly LiveSessionSubscriptionSet _subscriptions = new();
|
|
private readonly Action<int>? _constructionCheckpoint;
|
|
private int _constructionStep;
|
|
private int _accepting = 1;
|
|
|
|
public LiveSessionEventRouter(
|
|
WorldSession session,
|
|
LiveEntitySessionSink entities,
|
|
LiveEnvironmentSessionSink environment,
|
|
LiveInventorySessionBindings inventory,
|
|
LiveCharacterSessionBindings character,
|
|
LiveSocialSessionBindings social,
|
|
Action<int>? constructionCheckpoint = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(session);
|
|
Validate(entities, environment, inventory, character, social);
|
|
_constructionCheckpoint = constructionCheckpoint;
|
|
|
|
try
|
|
{
|
|
// Preserve the shipped pre-Connect registration order. Property
|
|
// state is installed before lifecycle packets can be dispatched.
|
|
_subscriptions.Add(ObjectTableWiring.Wire(
|
|
session,
|
|
inventory.Objects,
|
|
inventory.PlayerGuid,
|
|
inventory.LocalPlayer,
|
|
IsAccepting));
|
|
ConstructionCheckpoint();
|
|
_subscriptions.Add(CombatStateWiring.Wire(
|
|
session,
|
|
character.Combat,
|
|
IsAccepting));
|
|
ConstructionCheckpoint();
|
|
|
|
Subscribe(h => session.EntitySpawned += h, h => session.EntitySpawned -= h, entities.Spawned);
|
|
Subscribe(h => session.EntityDeleted += h, h => session.EntityDeleted -= h, entities.Deleted);
|
|
Subscribe(h => session.EntityPickedUp += h, h => session.EntityPickedUp -= h, entities.PickedUp);
|
|
Subscribe(h => session.MotionUpdated += h, h => session.MotionUpdated -= h, entities.MotionUpdated);
|
|
Subscribe(h => session.PositionUpdated += h, h => session.PositionUpdated -= h, entities.PositionUpdated);
|
|
Subscribe(h => session.VectorUpdated += h, h => session.VectorUpdated -= h, entities.VectorUpdated);
|
|
Subscribe(h => session.StateUpdated += h, h => session.StateUpdated -= h, entities.StateUpdated);
|
|
Subscribe(h => session.ParentUpdated += h, h => session.ParentUpdated -= h, entities.ParentUpdated);
|
|
Subscribe(h => session.TeleportStarted += h, h => session.TeleportStarted -= h, entities.TeleportStarted);
|
|
Subscribe(h => session.AppearanceUpdated += h, h => session.AppearanceUpdated -= h, entities.AppearanceUpdated);
|
|
Subscribe(
|
|
h => session.PlayPhysicsScriptReceived += h,
|
|
h => session.PlayPhysicsScriptReceived -= h,
|
|
entities.PlayPhysicsScript);
|
|
Subscribe(
|
|
h => session.PlayPhysicsScriptTypeReceived += h,
|
|
h => session.PlayPhysicsScriptTypeReceived -= h,
|
|
entities.PlayPhysicsScriptType);
|
|
|
|
Subscribe(h => session.EnvironChanged += h, h => session.EnvironChanged -= h, environment.EnvironChanged);
|
|
Subscribe(h => session.ServerTimeUpdated += h, h => session.ServerTimeUpdated -= h, environment.ServerTimeUpdated);
|
|
|
|
_subscriptions.Add(GameEventWiring.WireAll(
|
|
session.GameEvents,
|
|
inventory.Objects,
|
|
character.Combat,
|
|
character.Spellbook,
|
|
social.Chat,
|
|
inventory.LocalPlayer,
|
|
social.TurbineChat,
|
|
onSkillsUpdated: character.OnSkillsUpdated,
|
|
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
|
onShortcuts: inventory.OnShortcuts,
|
|
playerGuid: inventory.PlayerGuid,
|
|
onUseDone: inventory.OnUseDone,
|
|
itemMana: inventory.ItemMana,
|
|
onConfirmationRequest: character.OnConfirmationRequest,
|
|
onConfirmationDone: character.OnConfirmationDone,
|
|
friends: social.Friends,
|
|
squelch: social.Squelch,
|
|
onDesiredComponents: inventory.OnDesiredComponents,
|
|
onCharacterOptions: character.OnCharacterOptions,
|
|
clientTime: character.ClientTime,
|
|
externalContainers: inventory.ExternalContainers,
|
|
accepting: IsAccepting));
|
|
ConstructionCheckpoint();
|
|
_subscriptions.Add(new CombatChatTranslator(
|
|
character.Combat,
|
|
social.Chat,
|
|
IsAccepting));
|
|
ConstructionCheckpoint();
|
|
|
|
Subscribe<HearSpeech.Parsed>(h => session.SpeechHeard += h, h => session.SpeechHeard -= h, speech =>
|
|
social.Chat.OnLocalSpeech(
|
|
speech.SenderName,
|
|
speech.Text,
|
|
speech.SenderGuid,
|
|
speech.IsRanged));
|
|
Subscribe<ServerMessage.Parsed>(
|
|
h => session.ServerMessageReceived += h,
|
|
h => session.ServerMessageReceived -= h,
|
|
message => social.Chat.OnSystemMessage(message.Message, message.ChatType));
|
|
Subscribe<EmoteText.Parsed>(h => session.EmoteHeard += h, h => session.EmoteHeard -= h, emote =>
|
|
social.Chat.OnEmote(emote.SenderName, emote.Text, emote.SenderGuid));
|
|
Subscribe<SoulEmote.Parsed>(h => session.SoulEmoteHeard += h, h => session.SoulEmoteHeard -= h, emote =>
|
|
social.Chat.OnSoulEmote(emote.SenderName, emote.Text, emote.SenderGuid));
|
|
Subscribe<PlayerKilled.Parsed>(
|
|
h => session.PlayerKilledReceived += h,
|
|
h => session.PlayerKilledReceived -= h,
|
|
killed => social.Chat.OnPlayerKilled(
|
|
killed.DeathMessage,
|
|
killed.VictimGuid,
|
|
killed.KillerGuid));
|
|
Subscribe<TurbineChat.Parsed>(
|
|
h => session.TurbineChatReceived += h,
|
|
h => session.TurbineChatReceived -= h,
|
|
parsed => RouteTurbineChat(social.Chat, parsed));
|
|
Subscribe<PrivateUpdateVital.ParsedFull>(h => session.VitalUpdated += h, h => session.VitalUpdated -= h, vital =>
|
|
inventory.LocalPlayer.OnVitalUpdate(
|
|
vital.VitalId,
|
|
vital.Ranks,
|
|
vital.Start,
|
|
vital.Xp,
|
|
vital.Current));
|
|
Subscribe<PrivateUpdateVital.ParsedCurrent>(
|
|
h => session.VitalCurrentUpdated += h,
|
|
h => session.VitalCurrentUpdated -= h,
|
|
vital => inventory.LocalPlayer.OnVitalCurrent(vital.VitalId, vital.Current));
|
|
}
|
|
catch (Exception constructionError)
|
|
{
|
|
Interlocked.Exchange(ref _accepting, 0);
|
|
try
|
|
{
|
|
_subscriptions.Dispose();
|
|
}
|
|
catch (Exception cleanupError)
|
|
{
|
|
throw new AggregateException(
|
|
"live-session event routing failed and cleanup also failed",
|
|
constructionError,
|
|
cleanupError);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public bool Accepting => IsAccepting();
|
|
|
|
public void Dispose()
|
|
{
|
|
Interlocked.Exchange(ref _accepting, 0);
|
|
_subscriptions.Dispose();
|
|
}
|
|
|
|
private void Subscribe<T>(
|
|
Action<Action<T>> attach,
|
|
Action<Action<T>> detach,
|
|
Action<T> sink)
|
|
{
|
|
Action<T> handler = value =>
|
|
{
|
|
if (Volatile.Read(ref _accepting) != 0)
|
|
sink(value);
|
|
};
|
|
|
|
attach(handler);
|
|
try
|
|
{
|
|
_subscriptions.Add(() => detach(handler));
|
|
}
|
|
catch
|
|
{
|
|
detach(handler);
|
|
throw;
|
|
}
|
|
|
|
ConstructionCheckpoint();
|
|
}
|
|
|
|
private void ConstructionCheckpoint() =>
|
|
_constructionCheckpoint?.Invoke(++_constructionStep);
|
|
|
|
private bool IsAccepting() => Volatile.Read(ref _accepting) != 0;
|
|
|
|
private static void RouteTurbineChat(ChatLog chat, TurbineChat.Parsed parsed)
|
|
{
|
|
if (parsed.Body is not TurbineChat.Payload.EventSendToRoom message)
|
|
return;
|
|
|
|
chat.OnChannelBroadcast(
|
|
message.RoomId,
|
|
message.SenderName,
|
|
message.Message,
|
|
TurbineChatRouting.DisplayName(message.RoomId, message.ChatType));
|
|
}
|
|
|
|
private static void Validate(
|
|
LiveEntitySessionSink entities,
|
|
LiveEnvironmentSessionSink environment,
|
|
LiveInventorySessionBindings inventory,
|
|
LiveCharacterSessionBindings character,
|
|
LiveSocialSessionBindings social)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entities);
|
|
ArgumentNullException.ThrowIfNull(environment);
|
|
ArgumentNullException.ThrowIfNull(inventory);
|
|
ArgumentNullException.ThrowIfNull(character);
|
|
ArgumentNullException.ThrowIfNull(social);
|
|
ArgumentNullException.ThrowIfNull(entities.Spawned);
|
|
ArgumentNullException.ThrowIfNull(entities.Deleted);
|
|
ArgumentNullException.ThrowIfNull(entities.PickedUp);
|
|
ArgumentNullException.ThrowIfNull(entities.MotionUpdated);
|
|
ArgumentNullException.ThrowIfNull(entities.PositionUpdated);
|
|
ArgumentNullException.ThrowIfNull(entities.VectorUpdated);
|
|
ArgumentNullException.ThrowIfNull(entities.StateUpdated);
|
|
ArgumentNullException.ThrowIfNull(entities.ParentUpdated);
|
|
ArgumentNullException.ThrowIfNull(entities.TeleportStarted);
|
|
ArgumentNullException.ThrowIfNull(entities.AppearanceUpdated);
|
|
ArgumentNullException.ThrowIfNull(entities.PlayPhysicsScript);
|
|
ArgumentNullException.ThrowIfNull(entities.PlayPhysicsScriptType);
|
|
ArgumentNullException.ThrowIfNull(environment.EnvironChanged);
|
|
ArgumentNullException.ThrowIfNull(environment.ServerTimeUpdated);
|
|
ArgumentNullException.ThrowIfNull(inventory.Objects);
|
|
ArgumentNullException.ThrowIfNull(inventory.LocalPlayer);
|
|
ArgumentNullException.ThrowIfNull(inventory.PlayerGuid);
|
|
ArgumentNullException.ThrowIfNull(character.Combat);
|
|
ArgumentNullException.ThrowIfNull(character.Spellbook);
|
|
ArgumentNullException.ThrowIfNull(social.Chat);
|
|
ArgumentNullException.ThrowIfNull(social.TurbineChat);
|
|
}
|
|
}
|
|
|
|
internal sealed class LiveSessionSubscriptionSet : IDisposable
|
|
{
|
|
private List<IDisposable>? _subscriptions = [];
|
|
|
|
public void Add(IDisposable subscription)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(subscription);
|
|
List<IDisposable>? subscriptions = _subscriptions;
|
|
if (subscriptions is null)
|
|
{
|
|
subscription.Dispose();
|
|
throw new ObjectDisposedException(nameof(LiveSessionSubscriptionSet));
|
|
}
|
|
|
|
subscriptions.Add(subscription);
|
|
}
|
|
|
|
public void Add(Action unsubscribe) => Add(new ActionSubscription(unsubscribe));
|
|
|
|
public void Dispose()
|
|
{
|
|
List<IDisposable>? subscriptions = Interlocked.Exchange(ref _subscriptions, null);
|
|
if (subscriptions is null)
|
|
return;
|
|
|
|
List<Exception>? errors = null;
|
|
for (int index = subscriptions.Count - 1; index >= 0; index--)
|
|
{
|
|
try
|
|
{
|
|
subscriptions[index].Dispose();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(errors ??= []).Add(error);
|
|
}
|
|
}
|
|
|
|
if (errors is not null)
|
|
throw new AggregateException(
|
|
"one or more live-session subscriptions failed to detach",
|
|
errors);
|
|
}
|
|
|
|
private sealed class ActionSubscription(Action unsubscribe) : IDisposable
|
|
{
|
|
private Action? _unsubscribe = unsubscribe;
|
|
|
|
public void Dispose() => Interlocked.Exchange(ref _unsubscribe, null)?.Invoke();
|
|
}
|
|
}
|