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
|
|
@ -1,3 +1,6 @@
|
|||
using System.Reflection;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests;
|
||||
|
||||
public sealed class GameRuntimeContractTests
|
||||
|
|
@ -137,4 +140,68 @@ public sealed class GameRuntimeContractTests
|
|||
Assert.Equal("General|Grey|hello", entry.Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceRecorderIncludesJ4GameplayStateInCheckpoint()
|
||||
{
|
||||
var recorder = new RuntimeTraceRecorder();
|
||||
var stamp = new RuntimeEventStamp(new(3), 1, 8);
|
||||
var checkpoint = new RuntimeStateCheckpoint(
|
||||
new RuntimeGenerationToken(3),
|
||||
RuntimeLifecycleState.InWorld,
|
||||
FrameNumber: 8,
|
||||
EntityCount: 2,
|
||||
MaterializedEntityCount: 2,
|
||||
InventoryObjectCount: 1,
|
||||
InventoryContainerCount: 1,
|
||||
new RuntimeInventoryStateSnapshot(
|
||||
0u, 0u, 0, true, null, 2, 4, 1, 3),
|
||||
new RuntimeCharacterSnapshot(
|
||||
CharacterRevision: 5,
|
||||
SpellbookRevision: 6,
|
||||
default,
|
||||
default,
|
||||
LearnedSpellCount: 7,
|
||||
ActiveEnchantmentCount: 0,
|
||||
DesiredComponentCount: 0,
|
||||
SkillCount: 8,
|
||||
SpellbookFilters: 0x3FFFu),
|
||||
new RuntimeSocialSnapshot(9, 10, 11, 0, 0, 0, 0),
|
||||
ChatRevision: 12,
|
||||
ChatCount: 13,
|
||||
default,
|
||||
default);
|
||||
|
||||
recorder.AddCheckpoint(stamp, checkpoint);
|
||||
|
||||
RuntimeTraceEntry entry = Assert.Single(recorder.Entries);
|
||||
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
|
||||
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
|
||||
Assert.Contains("social=9:10:11", entry.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void J4GameplayOwnersHaveNoStaticMutableSessionState()
|
||||
{
|
||||
Type[] owners =
|
||||
[
|
||||
typeof(RuntimeCommunicationState),
|
||||
typeof(RuntimeInventoryState),
|
||||
typeof(RuntimeCharacterState),
|
||||
typeof(RuntimeCharacterOptionsState),
|
||||
typeof(RuntimeMovementSkillState),
|
||||
];
|
||||
|
||||
foreach (Type owner in owners)
|
||||
{
|
||||
FieldInfo[] mutable = owner
|
||||
.GetFields(
|
||||
BindingFlags.Static
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.NonPublic)
|
||||
.Where(static field => !field.IsLiteral)
|
||||
.ToArray();
|
||||
Assert.Empty(mutable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
|
@ -152,4 +153,105 @@ public sealed class RuntimeCharacterStateTests
|
|||
|
||||
Assert.True(state.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnsRetailCharacterOptionsAndMovementSkillProjection()
|
||||
{
|
||||
using var state = new RuntimeCharacterState();
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions1,
|
||||
state.Options.Options1);
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions2,
|
||||
state.Options.Options2);
|
||||
Assert.False(state.MovementSkills.IsComplete);
|
||||
|
||||
state.Options.Replace(0x04000000u, 0x12345678u);
|
||||
state.MovementSkills.Update(runSkill: 210, jumpSkill: -1);
|
||||
state.MovementSkills.Update(runSkill: -1, jumpSkill: 165);
|
||||
|
||||
Assert.True(state.Options.DragItemOnPlayerOpensSecureTrade);
|
||||
Assert.Equal(0x12345678u, state.Options.Options2);
|
||||
Assert.Equal(
|
||||
new RuntimeMovementSkillSnapshot(
|
||||
210,
|
||||
165,
|
||||
state.MovementSkills.Revision),
|
||||
state.MovementSkills.Snapshot);
|
||||
Assert.True(state.MovementSkills.IsComplete);
|
||||
|
||||
state.ResetSession();
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions1,
|
||||
state.Options.Options1);
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions2,
|
||||
state.Options.Options2);
|
||||
Assert.Equal(-1, state.MovementSkills.RunSkill);
|
||||
Assert.Equal(-1, state.MovementSkills.JumpSkill);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterViewBorrowsExactOwnersWithoutReconstructedState()
|
||||
{
|
||||
using var state = new RuntimeCharacterState();
|
||||
state.LocalPlayer.OnAttributeUpdate(1u, 40u, 10u, 500u);
|
||||
state.LocalPlayer.OnVitalUpdate(7u, 60u, 20u, 700u, 75u);
|
||||
state.LocalPlayer.OnSkillUpdate(
|
||||
6u,
|
||||
30u,
|
||||
2u,
|
||||
800u,
|
||||
10u,
|
||||
0u,
|
||||
5d,
|
||||
12u);
|
||||
state.Spellbook.OnSpellLearned(42u);
|
||||
state.Spellbook.SetFavorite(0, 0, 42u);
|
||||
state.Spellbook.SetDesiredComponent(0x68000001u, 11u);
|
||||
|
||||
RuntimeCharacterSnapshot summary = state.View.Snapshot;
|
||||
|
||||
Assert.Equal(1, summary.LearnedSpellCount);
|
||||
Assert.Equal(1, summary.DesiredComponentCount);
|
||||
Assert.Equal(1, summary.SkillCount);
|
||||
Assert.True(state.View.KnowsSpell(42u));
|
||||
Assert.True(state.View.TryGetFavorite(0, 0, out uint favorite));
|
||||
Assert.Equal(42u, favorite);
|
||||
Assert.True(state.View.TryGetDesiredComponent(
|
||||
0x68000001u,
|
||||
out uint desired));
|
||||
Assert.Equal(11u, desired);
|
||||
Assert.True(state.View.TryGetAttribute(
|
||||
(int)LocalPlayerState.AttributeKind.Strength,
|
||||
out RuntimeAttributeSnapshot attribute));
|
||||
Assert.Equal(50u, attribute.Current);
|
||||
Assert.True(state.View.TryGetVital(
|
||||
(int)LocalPlayerState.VitalKind.Health,
|
||||
out RuntimeVitalSnapshot vital));
|
||||
Assert.Equal(75u, vital.Current);
|
||||
Assert.True(state.View.TryGetSkill(6u, out RuntimeSkillSnapshot skill));
|
||||
Assert.Equal(52u, skill.CurrentLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoRuntimeInstancesIsolateOptionsSkillsAndViewRevisions()
|
||||
{
|
||||
using var first = new RuntimeCharacterState();
|
||||
using var second = new RuntimeCharacterState();
|
||||
|
||||
first.Options.Replace(1u, 2u);
|
||||
first.MovementSkills.Update(100, 200);
|
||||
first.Spellbook.OnSpellLearned(9u);
|
||||
|
||||
Assert.NotEqual(
|
||||
first.View.Snapshot.Options,
|
||||
second.View.Snapshot.Options);
|
||||
Assert.True(first.View.Snapshot.MovementSkills.IsComplete);
|
||||
Assert.False(second.View.Snapshot.MovementSkills.IsComplete);
|
||||
Assert.True(first.View.Snapshot.SpellbookRevision > 0);
|
||||
Assert.Equal(0, second.View.Snapshot.SpellbookRevision);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,44 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
|||
|
||||
public sealed class RuntimeCommunicationStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void SocialViewBorrowsFriendsAndSquelchOwners()
|
||||
{
|
||||
using var state = new RuntimeCommunicationState();
|
||||
state.Friends.Apply(new AcDream.Core.Social.FriendsUpdate(
|
||||
AcDream.Core.Social.FriendsUpdateType.Full,
|
||||
[
|
||||
new AcDream.Core.Social.FriendEntry(
|
||||
0x50000001u,
|
||||
"Friend",
|
||||
Online: true,
|
||||
AppearOffline: false,
|
||||
Friends: [],
|
||||
FriendOf: []),
|
||||
]));
|
||||
state.Squelch.Replace(new AcDream.Core.Social.SquelchDatabase(
|
||||
new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["account"] = 1u,
|
||||
},
|
||||
new Dictionary<uint, AcDream.Core.Social.SquelchInfo>(),
|
||||
new AcDream.Core.Social.SquelchInfo(
|
||||
string.Empty,
|
||||
false,
|
||||
new HashSet<uint> { 3u })));
|
||||
|
||||
RuntimeSocialSnapshot snapshot = state.SocialView.Snapshot;
|
||||
|
||||
Assert.Equal(1, snapshot.FriendCount);
|
||||
Assert.Equal(1, snapshot.SquelchedAccountCount);
|
||||
Assert.Equal(1, snapshot.GlobalSquelchTypeCount);
|
||||
Assert.True(state.SocialView.TryGetFriend(
|
||||
0x50000001u,
|
||||
out RuntimeFriendSnapshot friend));
|
||||
Assert.Equal("Friend", friend.Name);
|
||||
Assert.True(friend.Online);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnsOneExactCommunicationGraphAndPublishesCommittedEntries()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,44 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
|||
|
||||
public sealed class RuntimeInventoryStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void ViewBorrowsTransactionShortcutAndManaOwners()
|
||||
{
|
||||
using var entities = new RuntimeEntityObjectLifetime();
|
||||
using var inventory = new RuntimeInventoryState(entities);
|
||||
inventory.Shortcuts.Replace(
|
||||
[
|
||||
new AcDream.Core.Items.ShortcutEntry(3, 0x80000001u, 0u),
|
||||
new AcDream.Core.Items.ShortcutEntry(4, 0u, 42u),
|
||||
]);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(
|
||||
0x80000001u,
|
||||
0.75f,
|
||||
valid: true);
|
||||
inventory.Transactions.IncrementBusyCount();
|
||||
|
||||
RuntimeInventoryStateSnapshot snapshot = inventory.View.Snapshot;
|
||||
|
||||
Assert.Equal(2, snapshot.ShortcutCount);
|
||||
Assert.Equal(1, snapshot.ItemManaCount);
|
||||
Assert.Equal(1, snapshot.BusyCount);
|
||||
Assert.False(snapshot.CanBeginRequest);
|
||||
Assert.True(inventory.View.TryGetShortcut(
|
||||
3,
|
||||
out RuntimeShortcutSnapshot itemShortcut));
|
||||
Assert.Equal(
|
||||
new RuntimeShortcutSnapshot(3, 0x80000001u, 0u),
|
||||
itemShortcut);
|
||||
Assert.True(inventory.View.TryGetShortcut(
|
||||
4,
|
||||
out RuntimeShortcutSnapshot spellShortcut));
|
||||
Assert.Equal(new RuntimeShortcutSnapshot(4, 0u, 42u), spellShortcut);
|
||||
Assert.True(inventory.View.TryGetItemMana(
|
||||
itemShortcut.ObjectId,
|
||||
out float fraction));
|
||||
Assert.Equal(0.75f, fraction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnsOneGameplayGraphOverExactEntityObjectTable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -242,7 +242,6 @@ public sealed class LiveSessionEventRouterTests
|
|||
OnSkillsUpdated: null,
|
||||
OnConfirmationRequest: null,
|
||||
OnConfirmationDone: null,
|
||||
OnCharacterOptions: null,
|
||||
ClientTime: () => 0d);
|
||||
|
||||
private static LiveSocialSessionBindings NewSocialBindings(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue