refactor(net): converge live session state reset

This commit is contained in:
Erik 2026-07-21 12:00:48 +02:00
parent 78a9223b65
commit 4f31a5085f
35 changed files with 1460 additions and 83 deletions

View file

@ -53,4 +53,27 @@ public sealed class ChatLogLocalGuidTests
Assert.Equal("You", log.Snapshot()[0].Sender);
Assert.Equal(ChatKind.RangedSpeech, log.Snapshot()[0].Kind);
}
[Fact]
public void ResetSessionIdentity_RetainsTranscriptButClearsGuidAndDedupeWindow()
{
var log = new ChatLog();
const uint oldGuid = 0x50000001u;
const uint newGuid = 0x50000002u;
log.SetLocalPlayerGuid(oldGuid);
log.OnSystemMessage("session boundary", 1u);
log.OnLocalSpeech("Old", "before", oldGuid, isRanged: false);
log.ResetSessionIdentity();
log.OnSystemMessage("session boundary", 1u);
log.OnLocalSpeech("Old", "after", oldGuid, isRanged: false);
log.SetLocalPlayerGuid(newGuid);
log.OnLocalSpeech("New", "new", newGuid, isRanged: false);
ChatEntry[] entries = log.Snapshot();
Assert.Equal(5, entries.Length);
Assert.Equal("You", entries[1].Sender);
Assert.Equal("Old", entries[3].Sender);
Assert.Equal("You", entries[4].Sender);
}
}

View file

@ -8,6 +8,32 @@ namespace AcDream.Core.Tests.Chat;
/// </summary>
public sealed class TurbineChatStateTests
{
[Fact]
public void Reset_ClearsRoomsAndRestartsSessionCookieAtOne()
{
var state = new TurbineChatState();
state.OnChannelsReceived(1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
Assert.Equal(1u, state.NextContextId());
Assert.Equal(2u, state.NextContextId());
state.Reset();
Assert.False(state.Enabled);
Assert.Equal(0u, state.AllegianceRoom);
Assert.Equal(0u, state.GeneralRoom);
Assert.Equal(0u, state.TradeRoom);
Assert.Equal(0u, state.LfgRoom);
Assert.Equal(0u, state.RoleplayRoom);
Assert.Equal(0u, state.OlthoiRoom);
Assert.Equal(0u, state.SocietyRoom);
Assert.Equal(0u, state.SocietyCelestialHandRoom);
Assert.Equal(0u, state.SocietyEldrytchWebRoom);
Assert.Equal(0u, state.SocietyRadiantBloodRoom);
Assert.Equal(1u, state.NextContextId());
state.Reset();
Assert.Equal(1u, state.NextContextId());
}
[Fact]
public void InitialState_DisabledAndZeroRooms_NextContextIdStartsAt1()
{

View file

@ -141,4 +141,31 @@ public sealed class CombatStateTests
Assert.Equal(0, state.TrackedTargetCount);
Assert.Equal(CombatMode.NonCombat, state.CurrentMode);
}
[Fact]
public void Clear_RetryRepublishesAndOneObserverCannotStarveAnother()
{
var state = new CombatState();
state.SetCombatMode(CombatMode.Melee);
bool fail = true;
int delivered = 0;
state.CombatModeChanged += _ =>
{
if (fail)
{
fail = false;
throw new InvalidOperationException("transient");
}
};
state.CombatModeChanged += mode =>
{
Assert.Equal(CombatMode.NonCombat, mode);
delivered++;
};
Assert.Throws<AggregateException>(state.Clear);
Assert.Equal(1, delivered);
state.Clear();
Assert.Equal(2, delivered);
}
}

View file

@ -68,6 +68,32 @@ public sealed class ExternalContainerStateTests
Assert.False(state.ApplyViewContents(2u));
}
[Fact]
public void Reset_RetryRepublishesAndOneObserverCannotStarveAnother()
{
var state = Open(1u);
bool fail = true;
int delivered = 0;
state.Changed += _ =>
{
if (fail)
{
fail = false;
throw new InvalidOperationException("transient");
}
};
state.Changed += transition =>
{
Assert.Equal(ExternalContainerTransitionKind.Reset, transition.Kind);
delivered++;
};
Assert.Throws<AggregateException>(() => state.Reset());
Assert.Equal(1, delivered);
Assert.False(state.Reset());
Assert.Equal(2, delivered);
}
private static ExternalContainerState Open(uint id)
{
var state = new ExternalContainerState();

View file

@ -436,4 +436,45 @@ public sealed class LocalPlayerStateTests
s.OnProperties(props);
Assert.False(s.DebitInt64Property(2u, 100L)); // already 0 — no negative banking
}
[Fact]
public void Clear_ReturnsEveryCharacterSnapshotToPreLoginState()
{
var s = new LocalPlayerState();
var properties = new PropertyBundle();
properties.Ints[1u] = 42;
s.OnVitalUpdate(7u, 1u, 2u, 3u, 4u);
s.OnAttributeUpdate(1u, 5u, 6u, 7u);
s.OnSkillUpdate(8u, 9u, 2u, 10u, 11u, 12u, 13d, 14u);
s.OnPositions(new Dictionary<uint, AcDream.Core.Physics.Position>
{
[1u] = new AcDream.Core.Physics.Position(
0xA9B40001u,
new System.Numerics.Vector3(1f, 2f, 3f),
System.Numerics.Quaternion.Identity),
});
s.OnProperties(properties);
int characterChanges = 0;
var vitalChanges = new List<LocalPlayerState.VitalKind>();
var attributeChanges = new List<LocalPlayerState.AttributeKind>();
s.CharacterChanged += () => characterChanges++;
s.Changed += vitalChanges.Add;
s.AttributeChanged += attributeChanges.Add;
s.Clear();
Assert.Null(s.Get(LocalPlayerState.VitalKind.Health));
Assert.Null(s.GetAttribute(LocalPlayerState.AttributeKind.Strength));
Assert.Empty(s.Skills);
Assert.Empty(s.Positions);
Assert.Empty(s.Properties.Ints);
Assert.Equal(Enum.GetValues<LocalPlayerState.VitalKind>(), vitalChanges);
Assert.Equal(Enum.GetValues<LocalPlayerState.AttributeKind>(), attributeChanges);
Assert.Equal(1, characterChanges);
s.Clear();
Assert.Null(s.Get(LocalPlayerState.VitalKind.Health));
Assert.Empty(s.Skills);
Assert.Empty(s.Properties.Ints);
}
}

View file

@ -24,6 +24,33 @@ public sealed class SelectionStateTests
Assert.False(state.SelectPrevious());
}
[Fact]
public void Reset_RetryRepublishesAndOneObserverCannotStarveAnother()
{
var state = new SelectionState();
state.Select(0x50000001u, SelectionChangeSource.World);
bool fail = true;
int delivered = 0;
state.Changed += _ =>
{
if (fail)
{
fail = false;
throw new InvalidOperationException("transient");
}
};
state.Changed += transition =>
{
Assert.Equal(SelectionChangeReason.SessionReset, transition.Reason);
delivered++;
};
Assert.Throws<AggregateException>(() => state.Reset());
Assert.Equal(1, delivered);
Assert.False(state.Reset());
Assert.Equal(2, delivered);
}
[Fact]
public void Select_CommitsOneTransitionAndTracksPreviousLikeRetail()
{

View file

@ -4,6 +4,30 @@ namespace AcDream.Core.Tests.Social;
public sealed class SocialStateTests
{
[Fact]
public void SquelchState_Clear_ReturnsEmptyDatabase()
{
var state = new SquelchState();
state.Replace(new SquelchDatabase(
new Dictionary<string, uint> { ["Account"] = 1u },
new Dictionary<uint, SquelchInfo>
{
[2u] = new("Character", false, new HashSet<uint> { 3u }),
},
new SquelchInfo("Global", true, new HashSet<uint> { 4u })));
state.Clear();
SquelchDatabase snapshot = state.Snapshot();
Assert.Empty(snapshot.Accounts);
Assert.Empty(snapshot.Characters);
Assert.Empty(snapshot.Global.MessageTypes);
Assert.Equal(string.Empty, snapshot.Global.Name);
Assert.False(snapshot.Global.AccountWide);
state.Clear();
Assert.Empty(state.Snapshot().Accounts);
}
[Fact]
public void FriendsState_AppliesRetailIncrementalUpdateKindsByObjectId()
{