fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -156,6 +156,43 @@ public sealed class LiveSessionEventRouterTests
|
|||
router.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerKilled_SuppressesLocalParticipantsAndKeepsBystanderLine()
|
||||
{
|
||||
const uint self = 0x50000001u;
|
||||
const uint other = 0x50000002u;
|
||||
const uint third = 0x50000003u;
|
||||
using var session = NewSession();
|
||||
var chat = new ChatLog();
|
||||
var social = new LiveSocialSessionBindings(
|
||||
chat,
|
||||
new TurbineChatState(),
|
||||
new FriendsState(),
|
||||
new SquelchState(),
|
||||
PlayerGuid: () => self);
|
||||
var router = new LiveSessionEventRouter(
|
||||
session,
|
||||
NoOpEntitySink(),
|
||||
NoOpEnvironmentSink(),
|
||||
NewInventoryBindings(),
|
||||
NewCharacterBindings(),
|
||||
social);
|
||||
router.Attach();
|
||||
|
||||
Action<PlayerKilled.Parsed> deliver =
|
||||
EventDelegate<Action<PlayerKilled.Parsed>>(
|
||||
session,
|
||||
nameof(session.PlayerKilledReceived));
|
||||
deliver(new PlayerKilled.Parsed("self died", self, other));
|
||||
deliver(new PlayerKilled.Parsed("self killed", other, self));
|
||||
deliver(new PlayerKilled.Parsed("bystander", other, third));
|
||||
|
||||
ChatEntry entry = Assert.Single(chat.Snapshot());
|
||||
Assert.Equal("bystander", entry.Text);
|
||||
Assert.Equal(other, entry.SenderGuid);
|
||||
Assert.Equal(third, entry.ChannelId);
|
||||
}
|
||||
|
||||
// ── Campaign FA slice FA2 fix-round SHOULD-FIX 4 (blast review) ────────
|
||||
// The single production registration site (LiveSessionEventRouter.cs)
|
||||
// was untested — both router-test factories default Fellowship/
|
||||
|
|
@ -878,6 +915,71 @@ public sealed class LiveSessionEventRouterTests
|
|||
PlayPhysicsScriptType: _ => { },
|
||||
SoundEvent: _ => { });
|
||||
|
||||
[Fact]
|
||||
public void HouseRentNotices_RouteIntoTheSharedRuntimeOwner()
|
||||
{
|
||||
const uint self = 0x50000001u;
|
||||
using var session = NewSession();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = self, Type = ItemType.Creature });
|
||||
var house = new RuntimeHouseState(objects);
|
||||
house.ApplyHouseData(
|
||||
new GameEvents.HouseData(
|
||||
BuyTime: 0u,
|
||||
RentTime: 1_700_000_000u,
|
||||
Type: 1u,
|
||||
MaintenanceFree: false,
|
||||
Buy: [],
|
||||
Rent: [new GameEvents.HousePayment(10, 10, 1u, "Pyreal", "Pyreals")],
|
||||
Position: new CreateObject.ServerPosition()),
|
||||
self);
|
||||
|
||||
var router = new LiveSessionEventRouter(
|
||||
session,
|
||||
NoOpEntitySink(),
|
||||
NoOpEnvironmentSink(),
|
||||
new LiveInventorySessionBindings(
|
||||
objects,
|
||||
PlayerGuid: () => self,
|
||||
OnShortcuts: null,
|
||||
OnUseDone: null,
|
||||
ItemMana: new ItemManaState(),
|
||||
ExternalContainers: new ExternalContainerState()),
|
||||
NewCharacterBindings(),
|
||||
new LiveSocialSessionBindings(
|
||||
new ChatLog(),
|
||||
new TurbineChatState(),
|
||||
new FriendsState(),
|
||||
new SquelchState(),
|
||||
House: house));
|
||||
router.Attach();
|
||||
|
||||
session.GameEvents.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapGameEvent(
|
||||
GameEventType.UpdateRentTime,
|
||||
BitConverter.GetBytes(1_710_000_000u)))!.Value);
|
||||
Assert.Equal("Rent:\n0/10 Pyreals", house.Lines[1]);
|
||||
Assert.Equal(HousePanelTextColor.RentNotPaid, house.PanelLines[^2].Color);
|
||||
|
||||
using var payload = new MemoryStream();
|
||||
using (var writer = new BinaryWriter(
|
||||
payload, System.Text.Encoding.UTF8, leaveOpen: true))
|
||||
{
|
||||
writer.Write(1u);
|
||||
writer.Write(10);
|
||||
writer.Write(10);
|
||||
writer.Write(1u);
|
||||
WriteString16L(writer, "Pyreal");
|
||||
WriteString16L(writer, "Pyreals");
|
||||
}
|
||||
session.GameEvents.Dispatch(GameEventEnvelope.TryParse(
|
||||
WrapGameEvent(GameEventType.UpdateRentPayment, payload.ToArray()))!.Value);
|
||||
|
||||
Assert.Equal("Rent:\n10/10 Pyreals", house.Lines[1]);
|
||||
Assert.Equal(HousePanelTextColor.RentPaid, house.PanelLines[^2].Color);
|
||||
router.Dispose();
|
||||
}
|
||||
|
||||
private static LiveEnvironmentSessionSink NoOpEnvironmentSink() => new(
|
||||
EnvironChanged: _ => { },
|
||||
ServerTimeUpdated: _ => { });
|
||||
|
|
@ -1017,6 +1119,24 @@ public sealed class LiveSessionEventRouterTests
|
|||
return body;
|
||||
}
|
||||
|
||||
private static byte[] WrapGameEvent(GameEventType type, byte[] payload)
|
||||
{
|
||||
byte[] body = new byte[GameEventEnvelope.HeaderSize + payload.Length];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, GameEventEnvelope.Opcode);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), 0u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), 0u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), (uint)type);
|
||||
payload.CopyTo(body, GameEventEnvelope.HeaderSize);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static void WriteString16L(BinaryWriter writer, string value)
|
||||
{
|
||||
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(value);
|
||||
writer.Write((ushort)bytes.Length);
|
||||
writer.Write(bytes);
|
||||
}
|
||||
|
||||
private static WorldSession NewSession() =>
|
||||
new(new IPEndPoint(IPAddress.Loopback, 9));
|
||||
|
||||
|
|
|
|||
|
|
@ -126,21 +126,18 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Rejected_WhenTheClassifierDeclinesTheFrame()
|
||||
public void Committed_WhenForceAuthorityCarriesANewerTeleportStamp()
|
||||
{
|
||||
using StartedRuntime started = StartRuntime();
|
||||
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
||||
(RuntimeEntityRecord record, _) =
|
||||
EnterLocalPlayer(started.Runtime);
|
||||
Vector3 positionBefore = controller.Position;
|
||||
RuntimeAcceptedPositionDriveController drive =
|
||||
CreateAcceptedPositionDrive(started.Runtime, out List<byte[]> gameActions);
|
||||
|
||||
// RuntimeAuthoritativePositionRouteClassifier.ValidAcceptedAuthority's
|
||||
// ForcePosition case requires PreviousTeleportSequence ==
|
||||
// AcceptedTeleportSequence; a mismatch is a genuine data-validity
|
||||
// rejection (this call site never re-derives the timestamp gate's
|
||||
// own freshness rule — it is asserting the classifier's own
|
||||
// independent structural check).
|
||||
// #325: retail Gate A accepts a ForcePosition whose teleport stamp is
|
||||
// equal OR newer. The classifier independently preserves that wider
|
||||
// authority set and still routes this as a force correction rather
|
||||
// than a teleport.
|
||||
RuntimeAcceptedPositionExecutionStatus status =
|
||||
drive.TryExecuteAcceptedLocalPosition(
|
||||
record,
|
||||
|
|
@ -149,9 +146,8 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|||
Timestamps(teleport: 6),
|
||||
previousTeleportSequence: 5);
|
||||
|
||||
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Rejected, status);
|
||||
Assert.Equal(positionBefore, controller.Position);
|
||||
Assert.Empty(gameActions);
|
||||
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status);
|
||||
Assert.Single(gameActions);
|
||||
AssertConverged(started.Runtime);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue