fix(app,headless): Campaign CC slice CC4 re-review round — R1 arbiter + R2-R4
CC4 re-review returned NOT CLOSED: R1 (MEDIUM, blocking) is a new residual the F1 fix itself introduced, plus three LOW riders (R2, R3, R4). R1 — nulling UiRoot.FixedCanvasSize on chargen Close() stripped it from character-management, which stays active underneath and only sets the canvas on its own activation edge. Root cause (reviewer-named): two controllers writing one host-global with no owner. Fixed with the root-cause shape (reviewer's option (c)): UiRoot.DeclareFixedCanvas(owner, size)/RevokeFixedCanvas(owner), an owner-scoped arbiter — every declarer must agree on the canvas size (a mismatch throws instead of silently last-writer-wins), and the canvas nulls only once EVERY declarer has revoked. Both CharacterCreationUiController and CharacterManagementUi- Controller now declare/revoke instead of writing FixedCanvasSize directly; grepped for stragglers, none remain in production code (the raw setter stays public only for UiRootFixedCanvasTests' isolated scale-math coverage). New test (reviewer-specified): CharacterScreensFixedCanvasArbiterTests — two controllers sharing one UiRoot, proving the canvas stays set through chargen's Exit-confirm Close while char-management is still active, nulling only once char-management also deactivates, plus the original F1 defect's own covering case (both revoke together at world entry). R3 — HeadlessSessionHostTests.ContentLease_InstallsRealChargenOptions_ SelectHeritageIsAccepted proves F6's install actually opens the gate: a content lease carrying a real hand-built DatCharGen heritage (not ChargenOptions.Empty) is installed, and TrySelectHeritage for it succeeds. R2 — filed docs/ISSUES.md #402 for the pre-existing Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate full-suite flake (unrelated to Campaign CC). R4 — fixed "unchached" -> "uncached" typo in InteractionRetainedUiComposition.cs. Runtime 1713/0, App 5127/13 skips (+2), Headless 166/0 (+1), full solution Release build green. Live-DAT probes 7/7 under ACDREAM_PROBE_LIVE_MOUNT=1. The known #402 flake did not fire across 3 consecutive full-suite runs this session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ec854db045
commit
8add0667a5
9 changed files with 780 additions and 29 deletions
|
|
@ -1,10 +1,15 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using AcDream.Content;
|
||||
using AcDream.Content.CharGen;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -19,11 +24,68 @@ using AcDream.Runtime.Gameplay;
|
|||
using AcDream.Runtime.Physics;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.Runtime.World;
|
||||
using DatCharGen = DatReaderWriter.DBObjs.CharGen;
|
||||
using DatHeritageGroupCG = DatReaderWriter.Types.HeritageGroupCG;
|
||||
using DatIDBObj = DatReaderWriter.Lib.IO.IDBObj;
|
||||
using DatDatabaseImpl = DatReaderWriter.DatDatabase;
|
||||
using DatPStringBaseByte = DatReaderWriter.Types.PStringBase<byte>;
|
||||
|
||||
namespace AcDream.Headless.Tests;
|
||||
|
||||
public sealed class HeadlessSessionHostTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Review fix round R3 (2026-08-15): proves the F6 fix actually opens
|
||||
/// the gate, not just that <c>InstallOptions</c> was called. A content
|
||||
/// lease carrying a REAL chargen table (one heritage that does NOT
|
||||
/// exist in <see cref="AcDream.Core.CharGen.ChargenOptions.Empty"/>) is
|
||||
/// installed by the host constructor; selecting that heritage — refused
|
||||
/// against the pre-F6 empty default — is ACCEPTED once the session
|
||||
/// reaches character-creation-active. <c>Begin</c>/<c>TrySelectHeritage</c>
|
||||
/// are called directly (both <c>internal</c>, reachable via this
|
||||
/// project's InternalsVisibleTo on <c>AcDream.Runtime</c>) to isolate the
|
||||
/// F6 wiring from the unrelated real-network handshake that would
|
||||
/// otherwise be needed to reach the same session state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ContentLease_InstallsRealChargenOptions_SelectHeritageIsAccepted()
|
||||
{
|
||||
var factory = new ChargenFixtureContentFactory();
|
||||
using var owner = new HeadlessProcessContentOwner(
|
||||
ContentDescriptor(),
|
||||
_ => { },
|
||||
factory);
|
||||
HeadlessProcessContentOwner.HeadlessProcessContentLease lease =
|
||||
owner.AcquireLease("chargen-fixture");
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var diagnosticsOutput = new StringWriter();
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(diagnosticsOutput),
|
||||
new FixtureSessionOperations(),
|
||||
contentLease: lease);
|
||||
|
||||
RuntimeCharacterCreationState creation =
|
||||
host.Runtime.Session.CharacterCreationState;
|
||||
|
||||
// F6 proof #1: the fixture heritage (absent from ChargenOptions.Empty)
|
||||
// is actually installed off the content lease's real Dats.
|
||||
Assert.True(creation.Options.HeritagesById.ContainsKey(
|
||||
ChargenFixtureContentFactory.HeritageId));
|
||||
|
||||
// F6 proof #2: with real options installed, selecting that heritage
|
||||
// is accepted once character-creation is active (Begin mirrors the
|
||||
// real wire trigger this unit test bypasses).
|
||||
creation.Begin(host.Runtime.Generation);
|
||||
Assert.True(creation.TrySelectHeritage(ChargenFixtureContentFactory.HeritageId));
|
||||
Assert.Equal(
|
||||
ChargenFixtureContentFactory.HeritageId,
|
||||
creation.View.Snapshot.HeritageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoginCommandsUseTheHeadlessLiveBusAndPreserveWireOrder()
|
||||
{
|
||||
|
|
@ -3685,4 +3747,140 @@ public sealed class HeadlessSessionHostTests
|
|||
}
|
||||
}
|
||||
|
||||
private static HeadlessContentDescriptor ContentDescriptor() => new()
|
||||
{
|
||||
DatDirectory = "fixture-dats",
|
||||
PreparedAssetPath = "fixture.pak",
|
||||
};
|
||||
|
||||
/// <summary>Review fix round R3 (2026-08-15): a content factory whose
|
||||
/// <c>Dats</c> is a REAL (hand-built, not DispatchProxy-stubbed)
|
||||
/// <see cref="IDatReaderWriter"/> serving one heritage off
|
||||
/// <see cref="ChargenTableReader.ChargenTableDid"/> — the existing
|
||||
/// <c>TestResourceProxy</c> pattern elsewhere in this project always
|
||||
/// returns null/default and can't serve typed record data.</summary>
|
||||
private sealed class ChargenFixtureContentFactory : IHeadlessProcessContentFactory
|
||||
{
|
||||
internal const uint HeritageId = 1u;
|
||||
|
||||
public HeadlessOpenedProcessContent Open(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic) =>
|
||||
new(
|
||||
new FixtureChargenDatReaderWriter(),
|
||||
DispatchProxy.Create<ITestPreparedSource, TestResourceProxy>(),
|
||||
MagicCatalog.Empty,
|
||||
ImmutableArray.CreateRange(new float[256]));
|
||||
}
|
||||
|
||||
/// <summary>Minimal <see cref="IDatReaderWriter"/> mirroring
|
||||
/// <c>ChargenTableReaderTests.EmptyDatReaderWriter</c>'s shape, except
|
||||
/// <see cref="Get{T}"/> serves a real <see cref="DatCharGen"/> for
|
||||
/// <see cref="ChargenTableReader.ChargenTableDid"/> — everything else
|
||||
/// still misses, matching the missing-table tolerance
|
||||
/// <see cref="ChargenTableReader.Load"/> already has tests for.</summary>
|
||||
private sealed class FixtureChargenDatReaderWriter : IDatReaderWriter
|
||||
{
|
||||
private readonly StubDatabase _db = new();
|
||||
private readonly DatCharGen _chargenTable = BuildChargenTable();
|
||||
|
||||
public string SourceDirectory => string.Empty;
|
||||
public IDatDatabase Portal => _db;
|
||||
public IDatDatabase Cell => _db;
|
||||
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
|
||||
new(new Dictionary<uint, IDatDatabase>());
|
||||
public IDatDatabase HighRes => _db;
|
||||
public IDatDatabase Language => _db;
|
||||
public IDatDatabase Local => _db;
|
||||
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
|
||||
new(new Dictionary<uint, uint>());
|
||||
public int PortalIteration => 0;
|
||||
public int CellIteration => 0;
|
||||
public int HighResIteration => 0;
|
||||
public int LanguageIteration => 0;
|
||||
|
||||
public bool TryGetFileBytes(
|
||||
uint regionId,
|
||||
uint fileId,
|
||||
ref byte[] bytes,
|
||||
out int bytesRead)
|
||||
{
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : DatIDBObj =>
|
||||
Array.Empty<uint>();
|
||||
|
||||
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
|
||||
Array.Empty<IDatReaderWriter.IdResolution>();
|
||||
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : DatIDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public bool TrySave<T>(uint regionId, T obj, int iteration = 0) where T : DatIDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
[return: MaybeNull]
|
||||
public T Get<T>(uint fileId) where T : DatIDBObj =>
|
||||
fileId == ChargenTableReader.ChargenTableDid
|
||||
&& typeof(T) == typeof(DatCharGen)
|
||||
? (T)(object)_chargenTable
|
||||
: default;
|
||||
|
||||
public bool TryGet<T>(uint fileId, [MaybeNullWhen(false)] out T value)
|
||||
where T : DatIDBObj
|
||||
{
|
||||
value = Get<T>(fileId);
|
||||
return value is not null;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
private static DatCharGen BuildChargenTable()
|
||||
{
|
||||
var table = new DatCharGen();
|
||||
var heritage = new DatHeritageGroupCG
|
||||
{
|
||||
Name = Str("FixtureHeritage"),
|
||||
AttributeCredits = 60u,
|
||||
SkillCredits = 50u,
|
||||
};
|
||||
table.HeritageGroups.Add(ChargenFixtureContentFactory.HeritageId, heritage);
|
||||
return table;
|
||||
}
|
||||
|
||||
private static DatPStringBaseByte Str(string value)
|
||||
{
|
||||
var s = new DatPStringBaseByte();
|
||||
s.Value = value;
|
||||
return s;
|
||||
}
|
||||
|
||||
private sealed class StubDatabase : IDatDatabase
|
||||
{
|
||||
public DatDatabaseImpl Db => null!;
|
||||
public int Iteration => 0;
|
||||
public IEnumerable<uint> GetAllIdsOfType<T>() where T : DatIDBObj =>
|
||||
Array.Empty<uint>();
|
||||
public bool TryGet<T>(uint fileId, [MaybeNullWhen(false)] out T value)
|
||||
where T : DatIDBObj
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
public bool TryGetFileBytes(uint fileId, [MaybeNullWhen(false)] out byte[] value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
public bool TryGetFileBytes(uint fileId, ref byte[] bytes, out int bytesRead)
|
||||
{
|
||||
bytesRead = 0;
|
||||
return false;
|
||||
}
|
||||
public bool TrySave<T>(T obj, int iteration = 0) where T : DatIDBObj => false;
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue