namespace AcDream.Core.Chat; /// /// Presentation-independent reply/retell targets derived from the committed /// chat transcript. /// /// /// 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 . This is the existing retail /// @reply/@retell behavior formerly owned by ChatVM. /// 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; } } public bool IsDisposed { get { lock (_gate) return _disposed; } } /// /// Forget character-scoped command targets without clearing visible /// transcript history. /// 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; } } }