fix: social gate round 2, part 2 - confirmation-dialog sentences + the
refused-drop yellow notice Item 4 (confirmation dialogs missing text + names): the missing retail mechanism was StringTable template substitution - an entry is N+1 literal fragments interleaved with N named variables, composed by StringTable::GetString @0x004300D0 (no-metalanguage branch @0x004303B7). ACE sends the bare player name for types 1/4; retail's OWN CLIENT wraps it. Ported as DatStringResolver.ResolveTemplate (PLAYER hash 0x05506DA2, the exact compute_str_hash space; Chorizite stores the variable hashes directly): - Server-driven type 4 -> ID_Fellowship_FellowshipRequest, type 1 -> ID_Allegiance_AcceptSwearConfirmation, injected into GameplayConfirmationController; null resolve falls back to the bare wire message, never invented English. The 2/3/5/6 " Continue?" family never consults the composer. - Local Swear/Break/Kick: the bind-time fragment-0 latch (which showed the dangling "Do you wish to swear to ") is replaced by click-time ResolveTemplate with the target's name. All five templates verified token-free in the installed DAT - this is NOT a StringTableMetaLanguage port (AD-81's engine caveat stands). Item 5 (refused drop shows nothing; retail shows yellow top-center text): the prevRequest latch was ALREADY ported (InventoryTransactionState); what was missing was the consumer. InventoryTransactionState now raises RequestFailed(request, weenieError) when a 0x00A0 clears the latch; ItemInteractionController composes ServerSaysAttemptFailed @0x0058EAE0's "The <item> can't be <verb>" (verb table + suffix map ported verbatim in Core's InventoryFailureMessages, NAME_PLURAL for merge/split) and routes it as LogTextType 0x1A ClientLocal -> the SpewBox, retail's yellow top-center line. The dispatcher's second leg (@0x0055B342) also runs: outside the 7-code exclusion set, WeenieErrorMessages resolves per-code text/destination; 0x426 AttunedItem has no row in either place beyond the verb line - faithful single-line output. Register: AD-85 narrowed to its numeric-field item, AD-81 amended (the token-free interleave is now ported; meta-token engine + FormatName remain), AD-93 filed (wire-guid-match vs retail's latched-guid preference; no Move/Wield latch kinds). Tests: +2 InventoryTransactionState failure-latch, +5 ResolveTemplate (constructed StringTable fixtures), +1 composer injection, +1 end-to-end refused-drop line. Core 4,697/1 skip, App 4,983/3 skips. Research: docs/research/2026-08-13-confirm-and-weenie-error-display.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fc62cb6397
commit
67fe754dd6
15 changed files with 1133 additions and 50 deletions
|
|
@ -41,9 +41,12 @@ public sealed class GameplayConfirmationControllerTests
|
|||
/// client reads neither option bit on the invite path;
|
||||
/// <c>RetailUiRuntime.HandleConfirmationRequest</c> now routes every
|
||||
/// type, including 4, straight here). Type 4 is NOT in the
|
||||
/// 2/3/5/6 " Continue?"-suffix set, so the message renders verbatim —
|
||||
/// matching <c>Handle_Character__ConfirmationRequest @0x005640A0</c>'s
|
||||
/// case-4 arm, which is a single call with no text transformation.
|
||||
/// 2/3/5/6 " Continue?"-suffix set, so with no composer injected the
|
||||
/// message renders verbatim — matching
|
||||
/// <c>Handle_Character__ConfirmationRequest @0x005640A0</c>'s case-4 arm.
|
||||
/// (Production now injects the StringTable template composer — see
|
||||
/// <see cref="InjectedComposerWrapsTypes1And4_AndNeverTouchesContinueFamily"/>;
|
||||
/// this test remains the null-composer fallback contract.)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FellowshipInviteRequest_Type4_OpensDialog_MessageVerbatim_AndSendsAcceptOnClose()
|
||||
|
|
@ -72,6 +75,65 @@ public sealed class GameplayConfirmationControllerTests
|
|||
Assert.Equal(0u, controller.ActiveDialogContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-13 social gate round 2 (the AD-85 narrowing): with an
|
||||
/// injected composer, types 1/4 render the StringTable-composed
|
||||
/// sentence instead of ACE's bare name; the 2/3/5/6 " Continue?" family
|
||||
/// never consults the composer; and a null compose result falls back to
|
||||
/// the bare wire message.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InjectedComposerWrapsTypes1And4_AndNeverTouchesContinueFamily()
|
||||
{
|
||||
var root = new UiRoot { Width = 800f, Height = 600f };
|
||||
ImportedLayout? shown = null;
|
||||
var factory = new RetailDialogFactory(root, _ =>
|
||||
shown = FixtureLoader.LoadConfirmationDialog());
|
||||
var composed = new List<uint>();
|
||||
using var controller = new GameplayConfirmationController(
|
||||
factory,
|
||||
(_, _, _) => { },
|
||||
(type, bareName) =>
|
||||
{
|
||||
composed.Add(type);
|
||||
return type == 4u
|
||||
? bareName
|
||||
+ " has invited you to join their fellowship. Do you accept?"
|
||||
: null;
|
||||
});
|
||||
|
||||
Assert.True(controller.HandleRequest(
|
||||
new GameEvents.CharacterConfirmationRequest(4u, 7u, "Alice")));
|
||||
Assert.Equal(
|
||||
"Alice has invited you to join their fellowship. Do you accept?",
|
||||
string.Join(" ", Assert.IsType<UiText>(shown!.FindElement(
|
||||
RetailConfirmationDialogView.MessageElementId)).LinesProvider()
|
||||
.Select(static line => line.Text)));
|
||||
Assert.IsType<UiButton>(shown.FindElement(
|
||||
RetailConfirmationDialogView.AcceptButtonId)).OnClick!();
|
||||
|
||||
// Null compose result (type 1 here) → the bare wire message.
|
||||
Assert.True(controller.HandleRequest(
|
||||
new GameEvents.CharacterConfirmationRequest(1u, 8u, "Bob")));
|
||||
Assert.Equal(
|
||||
"Bob",
|
||||
string.Join(" ", Assert.IsType<UiText>(shown!.FindElement(
|
||||
RetailConfirmationDialogView.MessageElementId)).LinesProvider()
|
||||
.Select(static line => line.Text)));
|
||||
Assert.IsType<UiButton>(shown.FindElement(
|
||||
RetailConfirmationDialogView.AcceptButtonId)).OnClick!();
|
||||
|
||||
// The " Continue?" family bypasses the composer entirely.
|
||||
Assert.True(controller.HandleRequest(
|
||||
new GameEvents.CharacterConfirmationRequest(2u, 9u, "Raise this skill?")));
|
||||
Assert.Equal(
|
||||
"Raise this skill? Continue?",
|
||||
string.Join(" ", Assert.IsType<UiText>(shown!.FindElement(
|
||||
RetailConfirmationDialogView.MessageElementId)).LinesProvider()
|
||||
.Select(static line => line.Text)));
|
||||
Assert.Equal([4u, 1u], composed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FA slice FA5, item 3: verifies the allegiance-swear
|
||||
/// confirmation (<c>ConfirmationType.AllegianceSwear</c>, type 1 —
|
||||
|
|
@ -83,12 +145,12 @@ public sealed class GameplayConfirmationControllerTests
|
|||
/// type 1 at all, but FA5's own contract calls for this explicit check
|
||||
/// since <c>SocialAllegiancePageController</c> is the new panel that
|
||||
/// makes this path reachable). Type 1 is NOT in the 2/3/5/6 " Continue?"
|
||||
/// suffix set, so the message renders verbatim — ACE's own type-1
|
||||
/// message is the target's BARE name (lane C §6.4:
|
||||
/// <c>Player_Allegiance.cs:91</c>/<c>ConfirmationManager.cs:38</c>), not
|
||||
/// a full sentence, which this test's message deliberately mirrors
|
||||
/// rather than inventing retail's unported <c>StringInfo</c>-wrapped
|
||||
/// sentence (AD-85).
|
||||
/// suffix set, so with no composer injected the message renders verbatim
|
||||
/// — ACE's own type-1 message is the target's BARE name (lane C §6.4:
|
||||
/// <c>Player_Allegiance.cs:91</c>/<c>ConfirmationManager.cs:38</c>).
|
||||
/// (Production now injects the StringTable template composer that wraps
|
||||
/// the name into retail's full sentence — the 2026-08-13 AD-85
|
||||
/// narrowing; this test remains the null-composer fallback contract.)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AllegianceSwearRequest_Type1_OpensDialog_MessageVerbatim_AndSendsAcceptOnClose()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -41,6 +42,7 @@ public sealed class ItemInteractionControllerTests
|
|||
public bool SendSellSucceeds = true;
|
||||
public readonly List<string> Toasts = new();
|
||||
public readonly List<string> SystemMessages = new();
|
||||
public readonly List<(string Text, RetailLogTextType Type)> InterfaceTexts = new();
|
||||
public readonly List<CombatMode> CombatModeRequests = new();
|
||||
public readonly CombatState Combat = new();
|
||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||
|
|
@ -127,7 +129,8 @@ public sealed class ItemInteractionControllerTests
|
|||
return false;
|
||||
Sells.Add((vendorGuid, items));
|
||||
return true;
|
||||
});
|
||||
},
|
||||
interfaceText: (text, type) => InterfaceTexts.Add((text, type)));
|
||||
}
|
||||
|
||||
public ItemInteractionController Controller { get; }
|
||||
|
|
@ -1413,6 +1416,39 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.Equal(0u, h.Objects.Get(0x50000A07u)!.ContainerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-13 social gate round 2, item 5: a refused drop (0x00A0,
|
||||
/// error 0x426 AttunedItem) composes ServerSaysAttemptFailed's
|
||||
/// "The <item> can't be dropped" and routes it as ClientLocal —
|
||||
/// the SpewBox's yellow top-center line. 0x426 has no HandleFailureEvent
|
||||
/// row, so exactly ONE line appears; a failure with no latched request
|
||||
/// shows nothing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RefusedDrop_ComposesCantBeDroppedLine_AsClientLocal()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A07u;
|
||||
h.AddContained(item);
|
||||
Assert.True(h.Controller.DropToWorld(new ItemDragPayload(
|
||||
item,
|
||||
ItemDragSource.Inventory,
|
||||
SourceSlot: 0,
|
||||
SourceCell: new UiItemSlot())));
|
||||
|
||||
h.Objects.RejectMove(item, 0x426u);
|
||||
|
||||
(string text, RetailLogTextType type) = Assert.Single(h.InterfaceTexts);
|
||||
Assert.Equal($"The Item {item:X} can't be dropped", text);
|
||||
Assert.Equal(RetailLogTextType.ClientLocal, type);
|
||||
|
||||
// No latched request → retail shows nothing (and 0x426 stays out of
|
||||
// the generic failure table).
|
||||
h.InterfaceTexts.Clear();
|
||||
h.Objects.RejectMove(item, 0x426u);
|
||||
Assert.Empty(h.InterfaceTexts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryDragOnNpc_sendsGiveWithoutOptimisticInventoryMutation()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Lib;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance for <see cref="DatStringResolver.ResolveTemplate"/> — the
|
||||
/// <c>StringTable::GetString @ 0x004300D0</c> fragment/variable interleave
|
||||
/// (no-metalanguage branch @ 0x004303B7) that composes the social
|
||||
/// confirmation sentences.
|
||||
/// </summary>
|
||||
public sealed class DatStringResolverTemplateTests
|
||||
{
|
||||
private const uint TableId = 0x23000001u;
|
||||
|
||||
[Fact]
|
||||
public void PlayerVariableIsTheRetailHash()
|
||||
=> Assert.Equal(0x05506DA2u, DatStringResolver.PlayerVariable);
|
||||
|
||||
[Fact]
|
||||
public void ComposesTrailingFragmentTemplate()
|
||||
{
|
||||
// ID_Allegiance_SwearConfirmation's shape:
|
||||
// ["Do you wish to swear to ", "?"] + [PLAYER]
|
||||
var resolver = MakeResolver(
|
||||
"ID_Allegiance_SwearConfirmation",
|
||||
fragments: ["Do you wish to swear to ", "?"],
|
||||
variables: [DatStringResolver.PlayerVariable]);
|
||||
|
||||
Assert.Equal(
|
||||
"Do you wish to swear to +Horan?",
|
||||
resolver.ResolveTemplate(
|
||||
TableId,
|
||||
"ID_Allegiance_SwearConfirmation",
|
||||
new Dictionary<uint, string>
|
||||
{
|
||||
[DatStringResolver.PlayerVariable] = "+Horan",
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComposesLeadingVariableTemplate()
|
||||
{
|
||||
// ID_Fellowship_FellowshipRequest's shape: an EMPTY first fragment,
|
||||
// so the player name leads the sentence.
|
||||
var resolver = MakeResolver(
|
||||
"ID_Fellowship_FellowshipRequest",
|
||||
fragments: [
|
||||
"",
|
||||
" has invited you to join their fellowship. Do you accept?",
|
||||
],
|
||||
variables: [DatStringResolver.PlayerVariable]);
|
||||
|
||||
Assert.Equal(
|
||||
"+Acdream has invited you to join their fellowship. Do you accept?",
|
||||
resolver.ResolveTemplate(
|
||||
TableId,
|
||||
"ID_Fellowship_FellowshipRequest",
|
||||
new Dictionary<uint, string>
|
||||
{
|
||||
[DatStringResolver.PlayerVariable] = "+Acdream",
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingVariableSubstitutesEmpty()
|
||||
{
|
||||
var resolver = MakeResolver(
|
||||
"ID_Allegiance_SwearConfirmation",
|
||||
fragments: ["Do you wish to swear to ", "?"],
|
||||
variables: [DatStringResolver.PlayerVariable]);
|
||||
|
||||
Assert.Equal(
|
||||
"Do you wish to swear to ?",
|
||||
resolver.ResolveTemplate(
|
||||
TableId,
|
||||
"ID_Allegiance_SwearConfirmation",
|
||||
new Dictionary<uint, string>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownKeyResolvesNull()
|
||||
{
|
||||
var resolver = MakeResolver(
|
||||
"ID_Allegiance_SwearConfirmation",
|
||||
fragments: ["Do you wish to swear to ", "?"],
|
||||
variables: [DatStringResolver.PlayerVariable]);
|
||||
|
||||
Assert.Null(resolver.ResolveTemplate(
|
||||
TableId, "ID_Not_A_Key", new Dictionary<uint, string>()));
|
||||
}
|
||||
|
||||
private static DatStringResolver MakeResolver(
|
||||
string key,
|
||||
string[] fragments,
|
||||
uint[] variables)
|
||||
{
|
||||
var entry = new StringTableString();
|
||||
foreach (string fragment in fragments)
|
||||
entry.Strings.Add(fragment);
|
||||
foreach (uint variable in variables)
|
||||
entry.Variables.Add(variable);
|
||||
|
||||
var table = new StringTable { Id = TableId };
|
||||
table.Strings[DatStringResolver.ComputeHash(key)] = entry;
|
||||
return new DatStringResolver(new SingleTableSource(table));
|
||||
}
|
||||
|
||||
/// <summary>Serves exactly one constructed StringTable through the
|
||||
/// production <see cref="IDatReaderWriter"/> seam.</summary>
|
||||
private sealed class SingleTableSource : IDatReaderWriter
|
||||
{
|
||||
private readonly StringTable _table;
|
||||
|
||||
public SingleTableSource(StringTable table) => _table = table;
|
||||
|
||||
public string SourceDirectory => string.Empty;
|
||||
public IDatDatabase Portal => throw new NotSupportedException();
|
||||
public IDatDatabase Cell => throw new NotSupportedException();
|
||||
public ReadOnlyDictionary<uint, IDatDatabase> CellRegions { get; } =
|
||||
new(new Dictionary<uint, IDatDatabase>());
|
||||
public IDatDatabase HighRes => throw new NotSupportedException();
|
||||
public IDatDatabase Language => throw new NotSupportedException();
|
||||
public IDatDatabase Local => throw new NotSupportedException();
|
||||
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 : IDBObj =>
|
||||
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 : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public bool TrySave<T>(
|
||||
uint regionId,
|
||||
T obj,
|
||||
int iteration = 0) where T : IDBObj =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
[return: MaybeNull]
|
||||
public T Get<T>(uint fileId) where T : IDBObj =>
|
||||
fileId == _table.Id && _table is T match ? match : default;
|
||||
|
||||
public bool TryGet<T>(
|
||||
uint fileId,
|
||||
[MaybeNullWhen(false)] out T value) where T : IDBObj
|
||||
{
|
||||
if (fileId == _table.Id && _table is T match)
|
||||
{
|
||||
value = match;
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance rows for the ServerSaysAttemptFailed @ 0x0058EAE0 port —
|
||||
/// verb table, suffix map, and the 0x00A0 dispatcher exclusion set
|
||||
/// (@ 0x0055B342).
|
||||
/// </summary>
|
||||
public sealed class InventoryFailureMessagesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(
|
||||
InventoryRequestKind.DropToWorld, "Bloodstone Chunk", 0x426u,
|
||||
"The Bloodstone Chunk can't be dropped")]
|
||||
[InlineData(
|
||||
InventoryRequestKind.Give, "Sword", 0u,
|
||||
"The Sword can't be given")]
|
||||
[InlineData(
|
||||
InventoryRequestKind.Pickup, "Sword", 0x2Au,
|
||||
"The Sword can't be picked up - you are too encumbered")]
|
||||
[InlineData(
|
||||
InventoryRequestKind.PutInContainer, "Sword", 0x3EEu,
|
||||
"The Sword can't be put in the container - the container is closed")]
|
||||
[InlineData(
|
||||
InventoryRequestKind.Merge, "Arrows", 0x1Du,
|
||||
"The Arrows can't be merged - you're too busy")]
|
||||
[InlineData(
|
||||
InventoryRequestKind.SplitToWorld, "Arrows", 0x38u,
|
||||
"The Arrows can't be split - unable to move to object")]
|
||||
[InlineData(
|
||||
InventoryRequestKind.SplitToContainer, "Arrows", 0x36u,
|
||||
"The Arrows can't be split - action cancelled")]
|
||||
public void ComposeMatchesServerSaysAttemptFailed(
|
||||
InventoryRequestKind kind,
|
||||
string name,
|
||||
uint error,
|
||||
string expected)
|
||||
=> Assert.Equal(expected, InventoryFailureMessages.Compose(kind, name, error));
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x1Eu)]
|
||||
[InlineData(0x2Bu)]
|
||||
[InlineData(0x3EFu)]
|
||||
[InlineData(0x43Eu)]
|
||||
[InlineData(0x4CEu)]
|
||||
[InlineData(0x4CFu)]
|
||||
[InlineData(0x46Au)]
|
||||
public void ExclusionSetSuppressesGenericFailureText(uint error)
|
||||
=> Assert.True(InventoryFailureMessages.SuppressesGenericFailureText(error));
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x426u)]
|
||||
[InlineData(0x1Du)]
|
||||
[InlineData(0u)]
|
||||
public void OtherErrorsDoNotSuppressGenericFailureText(uint error)
|
||||
=> Assert.False(InventoryFailureMessages.SuppressesGenericFailureText(error));
|
||||
}
|
||||
|
|
@ -240,6 +240,50 @@ public sealed class InventoryTransactionStateTests
|
|||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectMoveFiresRequestFailedWithLatchedKindAndWireError()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
var failures = new List<(PendingInventoryRequest Request, uint Error)>();
|
||||
state.RequestFailed += (request, error) => failures.Add((request, error));
|
||||
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.DropToWorld, First, static () => true));
|
||||
objects.RejectMove(First, 0x426u);
|
||||
|
||||
(PendingInventoryRequest failed, uint error) = Assert.Single(failures);
|
||||
Assert.Equal(InventoryRequestKind.DropToWorld, failed.Kind);
|
||||
Assert.Equal(First, failed.ItemId);
|
||||
Assert.Equal(0x426u, error);
|
||||
Assert.False(state.HasPendingRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequestFailedRequiresTheLatchedGuidAndAnActivePending()
|
||||
{
|
||||
var objects = CreateTable();
|
||||
using var state = new InventoryTransactionState(objects);
|
||||
int failures = 0;
|
||||
state.RequestFailed += (_, _) => failures++;
|
||||
|
||||
Assert.True(state.TryDispatch(
|
||||
InventoryRequestKind.DropToWorld, First, static () => true));
|
||||
|
||||
// A failure for a DIFFERENT item must not consume (or mislabel) the
|
||||
// latch — the stale-latch guard on retail's latched-guid preference.
|
||||
objects.RejectMove(Second, 0x426u);
|
||||
Assert.Equal(0, failures);
|
||||
Assert.True(state.HasPendingRequest);
|
||||
|
||||
objects.RejectMove(First, 0x426u);
|
||||
Assert.Equal(1, failures);
|
||||
|
||||
// RecordResponse cleared the latch; a repeat failure shows nothing.
|
||||
objects.RejectMove(First, 0x1Du);
|
||||
Assert.Equal(1, failures);
|
||||
}
|
||||
|
||||
private static ClientObjectTable CreateTable()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue