1
0

SimplePredictReconcileTest.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. #nullable enable
  2. using System.Collections.Generic;
  3. using System.Numerics;
  4. using Robust.Client.GameStates;
  5. using Robust.Client.Timing;
  6. using Robust.Shared;
  7. using Robust.Shared.Analyzers;
  8. using Robust.Shared.Configuration;
  9. using Robust.Shared.GameObjects;
  10. using Robust.Shared.GameStates;
  11. using Robust.Shared.IoC;
  12. using Robust.Shared.Map;
  13. using Robust.Shared.Serialization;
  14. using Robust.Shared.Timing;
  15. namespace Content.IntegrationTests.Tests.Networking
  16. {
  17. // This test checks that the prediction & reconciling system is working correctly with a simple boolean flag.
  18. // An entity system sets a flag on a networked component via a RaisePredictiveEvent,
  19. // so it runs predicted on client and eventually on server.
  20. // All the tick values are checked to ensure it arrives on client & server at the exact correct ticks.
  21. // On the client, the reconciling system is checked to ensure that the state correctly reset every tick,
  22. // until the server acknowledges it.
  23. // Then, the same test is performed again, but the server does not handle the message (it ignores it).
  24. // To simulate a mispredict.
  25. // This means the client is forced to reset it once it gets to the server tick where the server didn't do anything.
  26. // the tick where the server *should* have, but did not, acknowledge the state change.
  27. // Finally, we run two events inside the prediction area to ensure reconciling does for incremental stuff.
  28. [TestFixture]
  29. public sealed class SimplePredictReconcileTest
  30. {
  31. [Test]
  32. public async Task Test()
  33. {
  34. await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true });
  35. var server = pair.Server;
  36. var client = pair.Client;
  37. var sMapManager = server.ResolveDependency<IMapManager>();
  38. var sEntityManager = server.ResolveDependency<IEntityManager>();
  39. var cEntityManager = client.ResolveDependency<IEntityManager>();
  40. var sGameTiming = server.ResolveDependency<IGameTiming>();
  41. var cGameTiming = client.ResolveDependency<IClientGameTiming>();
  42. var cGameStateManager = client.ResolveDependency<IClientGameStateManager>();
  43. var cfg = client.ResolveDependency<IConfigurationManager>();
  44. var log = cfg.GetCVar(CVars.NetLogging);
  45. Assert.That(cfg.GetCVar(CVars.NetInterp), Is.True);
  46. EntityUid serverEnt = default;
  47. PredictionTestComponent serverComponent = default!;
  48. PredictionTestComponent clientComponent = default!;
  49. var serverSystem = sEntityManager.System<PredictionTestEntitySystem>();
  50. var clientSystem = cEntityManager.System<PredictionTestEntitySystem>();
  51. var sMapSys = sEntityManager.System<SharedMapSystem>();
  52. await server.WaitPost(() =>
  53. {
  54. // Spawn dummy component entity.
  55. sMapSys.CreateMap(out var map);
  56. serverEnt = sEntityManager.SpawnEntity(null, new MapCoordinates(new Vector2(0, 0), map));
  57. serverComponent = sEntityManager.AddComponent<PredictionTestComponent>(serverEnt);
  58. });
  59. // Run some ticks and ensure that the buffer has filled up.
  60. await pair.SyncTicks();
  61. await pair.RunTicksSync(25);
  62. Assert.That(cGameTiming.TickTimingAdjustment, Is.EqualTo(0));
  63. Assert.That(sGameTiming.TickTimingAdjustment, Is.EqualTo(0));
  64. // Check client buffer is full
  65. Assert.That(cGameStateManager.GetApplicableStateCount(), Is.EqualTo(cGameStateManager.TargetBufferSize));
  66. Assert.That(cGameStateManager.TargetBufferSize, Is.EqualTo(2));
  67. // This isn't required anymore, but the test had this for the sake of "technical things", and I cbf shifting
  68. // all the tick times over. So it stays.
  69. // For the record, the old comment on this test literally just mumbled something about "Due to technical things ...".
  70. // I love helpful comments.
  71. await client.WaitRunTicks(1);
  72. await client.WaitPost(() =>
  73. {
  74. clientComponent = cEntityManager.GetComponent<PredictionTestComponent>(cEntityManager.GetEntity(sEntityManager.GetNetEntity(serverEnt)));
  75. });
  76. var baseTick = sGameTiming.CurTick.Value;
  77. var delta = cGameTiming.CurTick.Value - baseTick;
  78. Assert.That(delta, Is.EqualTo(2));
  79. // When we expect the client to receive the message.
  80. var expected = new GameTick(baseTick + delta);
  81. Assert.Multiple(() =>
  82. {
  83. Assert.That(clientComponent.Foo, Is.False);
  84. // KEEP IN MIND WHEN READING THIS.
  85. // The game loop increments CurTick AFTER running the tick.
  86. // So when reading CurTick inside an Assert or Post or whatever, the tick reported is the NEXT one to run.
  87. Assert.That(serverComponent.Foo, Is.False);
  88. // Client last ran tick 15 meaning it's ahead of the last server tick it processed (12)
  89. Assert.That(cGameTiming.CurTick, Is.EqualTo(expected));
  90. Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint) (baseTick - cGameStateManager.TargetBufferSize))));
  91. });
  92. // *** I am using block scopes to visually distinguish these sections of the test to make it more readable.
  93. // Send an event to change the flag and instantly see the effect replicate client side,
  94. // while it's queued on server and reconciling works (constantly needs re-firing on client).
  95. {
  96. Assert.That(clientComponent.Foo, Is.False);
  97. await client.WaitPost(() =>
  98. {
  99. cEntityManager.RaisePredictiveEvent(new SetFooMessage(sEntityManager.GetNetEntity(serverEnt), true));
  100. });
  101. Assert.That(clientComponent.Foo, Is.True);
  102. // Event correctly arrived on client system.
  103. Assert.That(clientSystem.EventTriggerList,
  104. Is.EquivalentTo(new[] { (clientReceive: expected, true, false, true, true) }));
  105. clientSystem.EventTriggerList.Clear();
  106. // Two ticks happen on both sides with nothing really "changing".
  107. // Server doesn't receive it yet,
  108. // client is still replaying the past prediction.
  109. for (var i = 0; i < 2; i++)
  110. {
  111. await server.WaitRunTicks(1);
  112. // Event did not arrive on server.
  113. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  114. await client.WaitRunTicks(1);
  115. // Event got repeated on client as a past prediction.
  116. Assert.That(clientSystem.EventTriggerList,
  117. Is.EquivalentTo(new[] { (clientReceive: expected, false, false, true, true) }));
  118. clientSystem.EventTriggerList.Clear();
  119. }
  120. {
  121. await server.WaitRunTicks(1);
  122. Assert.Multiple(() =>
  123. {
  124. // Event arrived on server at tick 16.
  125. Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 3)));
  126. Assert.That(serverSystem.EventTriggerList,
  127. Is.EquivalentTo(new[] { (clientReceive: expected, true, false, true, true) }));
  128. });
  129. serverSystem.EventTriggerList.Clear();
  130. await client.WaitRunTicks(1);
  131. // Event got repeated on client as a past prediction.
  132. Assert.That(clientSystem.EventTriggerList,
  133. Is.EquivalentTo(new[] { (clientReceive: expected, false, false, true, true) }));
  134. clientSystem.EventTriggerList.Clear();
  135. }
  136. {
  137. await server.WaitRunTicks(1);
  138. // Nothing happened on server.
  139. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  140. await client.WaitRunTicks(1);
  141. Assert.Multiple(() =>
  142. {
  143. // Event got repeated on client as a past prediction.
  144. Assert.That(clientSystem.EventTriggerList, Is.Empty);
  145. Assert.That(clientComponent.Foo, Is.True);
  146. });
  147. clientSystem.EventTriggerList.Clear();
  148. }
  149. }
  150. // Disallow changes to simulate a misprediction.
  151. serverSystem.Allow = false;
  152. Assert.Multiple(() =>
  153. {
  154. // Assert timing is still correct, should be but it's a good reference for the rest of the test.
  155. Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 4)));
  156. Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 4 + delta)));
  157. Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(expected));
  158. });
  159. {
  160. // Send event to server to change flag again, this time to disable it..
  161. await client.WaitPost(() =>
  162. {
  163. cEntityManager.RaisePredictiveEvent(new SetFooMessage(sEntityManager.GetNetEntity(serverEnt), false));
  164. Assert.That(clientComponent.Foo, Is.False);
  165. });
  166. // Event correctly arrived on client system.
  167. Assert.That(clientSystem.EventTriggerList,
  168. Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), true, true, false, false) }));
  169. clientSystem.EventTriggerList.Clear();
  170. for (var i = 0; i < 2; i++)
  171. {
  172. await server.WaitRunTicks(1);
  173. // Event did not arrive on server.
  174. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  175. await client.WaitRunTicks(1);
  176. // Event got repeated on client as a past prediction.
  177. Assert.That(clientSystem.EventTriggerList,
  178. Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), false, true, false, false) }));
  179. clientSystem.EventTriggerList.Clear();
  180. }
  181. {
  182. await server.WaitRunTicks(1);
  183. Assert.Multiple(() =>
  184. {
  185. // Event arrived on server at tick 20.
  186. Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 7)));
  187. // But the server didn't listen!
  188. Assert.That(serverSystem.EventTriggerList,
  189. Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), true, true, true, false) }));
  190. });
  191. serverSystem.EventTriggerList.Clear();
  192. await client.WaitRunTicks(1);
  193. // Event got repeated on client as a past prediction.
  194. Assert.That(clientSystem.EventTriggerList,
  195. Is.EquivalentTo(new[] { (new GameTick(baseTick + 6), false, true, false, false) }));
  196. clientSystem.EventTriggerList.Clear();
  197. }
  198. {
  199. await server.WaitRunTicks(1);
  200. // Nothing happened on server.
  201. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  202. await client.WaitRunTicks(1);
  203. Assert.Multiple(() =>
  204. {
  205. // Event no longer got repeated and flag was *not* set by server state.
  206. // Mispredict gracefully handled!
  207. Assert.That(clientSystem.EventTriggerList, Is.Empty);
  208. Assert.That(clientComponent.Foo, Is.True);
  209. });
  210. clientSystem.EventTriggerList.Clear();
  211. }
  212. }
  213. // Re-allow changes to make everything work correctly again.
  214. serverSystem.Allow = true;
  215. Assert.Multiple(() =>
  216. {
  217. // Assert timing is still correct.
  218. Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 8)));
  219. Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 8 + delta)));
  220. Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint) (baseTick + 8 - cGameStateManager.TargetBufferSize))));
  221. });
  222. {
  223. // Send first event to disable the flag (reminder: it never got accepted by the server).
  224. await client.WaitPost(() =>
  225. {
  226. cEntityManager.RaisePredictiveEvent(new SetFooMessage(sEntityManager.GetNetEntity(serverEnt), false));
  227. Assert.That(clientComponent.Foo, Is.False);
  228. });
  229. // Event correctly arrived on client system.
  230. Assert.That(clientSystem.EventTriggerList,
  231. Is.EquivalentTo(new[] { (new GameTick(baseTick + 10), true, true, false, false) }));
  232. clientSystem.EventTriggerList.Clear();
  233. // Run one tick, everything checks out.
  234. {
  235. await server.WaitRunTicks(1);
  236. // Event did not arrive on server.
  237. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  238. await client.WaitRunTicks(1);
  239. // Event got repeated on client as a past prediction.
  240. Assert.That(clientSystem.EventTriggerList,
  241. Is.EquivalentTo(new[] { (new GameTick(baseTick + 10), false, true, false, false) }));
  242. clientSystem.EventTriggerList.Clear();
  243. }
  244. // Send another event, to re-enable it.
  245. await client.WaitPost(() =>
  246. {
  247. cEntityManager.RaisePredictiveEvent(new SetFooMessage(sEntityManager.GetNetEntity(serverEnt), true));
  248. Assert.That(clientComponent.Foo, Is.True);
  249. });
  250. // Event correctly arrived on client system.
  251. Assert.That(clientSystem.EventTriggerList,
  252. Is.EquivalentTo(new[] { (new GameTick(baseTick + 11), true, false, true, true) }));
  253. clientSystem.EventTriggerList.Clear();
  254. // Next tick we run, both events come in, but at different times.
  255. {
  256. await server.WaitRunTicks(1);
  257. // Event did not arrive on server.
  258. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  259. await client.WaitRunTicks(1);
  260. // Event got repeated on client as a past prediction.
  261. Assert.That(clientSystem.EventTriggerList,
  262. Is.EquivalentTo(new[]
  263. {
  264. (new GameTick(baseTick + 10), false, true, false, false), (new GameTick(baseTick + 11), false, false, true, true)
  265. }));
  266. clientSystem.EventTriggerList.Clear();
  267. }
  268. // FIRST event arrives on server!
  269. {
  270. await server.WaitRunTicks(1);
  271. Assert.That(serverSystem.EventTriggerList,
  272. Is.EquivalentTo(new[] { (new GameTick(baseTick + 10), true, true, false, false) }));
  273. serverSystem.EventTriggerList.Clear();
  274. await client.WaitRunTicks(1);
  275. // Event got repeated on client as a past prediction.
  276. Assert.That(clientSystem.EventTriggerList,
  277. Is.EquivalentTo(new[]
  278. {
  279. (new GameTick(baseTick + 10), false, true, false, false), (new GameTick(baseTick + 11), false, false, true, true)
  280. }));
  281. clientSystem.EventTriggerList.Clear();
  282. }
  283. // SECOND event arrived on server, client receives ack for first event,
  284. // still runs second event as past prediction.
  285. {
  286. await server.WaitRunTicks(1);
  287. Assert.That(serverSystem.EventTriggerList,
  288. Is.EquivalentTo(new[] { (new GameTick(baseTick + 11), true, false, true, true) }));
  289. serverSystem.EventTriggerList.Clear();
  290. await client.WaitRunTicks(1);
  291. // Event got repeated on client as a past prediction.
  292. Assert.That(clientSystem.EventTriggerList,
  293. Is.EquivalentTo(new[]
  294. {
  295. (new GameTick(baseTick + 11), false, false, true, true)
  296. }));
  297. clientSystem.EventTriggerList.Clear();
  298. }
  299. // Finally, second event acknowledged on client and we're good.
  300. {
  301. await server.WaitRunTicks(1);
  302. Assert.That(serverSystem.EventTriggerList, Is.Empty);
  303. await client.WaitRunTicks(1);
  304. Assert.Multiple(() =>
  305. {
  306. // Event got repeated on client as a past prediction.
  307. Assert.That(clientSystem.EventTriggerList, Is.Empty);
  308. Assert.That(clientComponent.Foo, Is.True);
  309. });
  310. }
  311. }
  312. cfg.SetCVar(CVars.NetLogging, log);
  313. await pair.CleanReturnAsync();
  314. }
  315. public sealed class PredictionTestEntitySystem : EntitySystem
  316. {
  317. public bool Allow { get; set; } = true;
  318. // Queue of all the events that come in so we can test that they come in perfectly as expected.
  319. public List<(GameTick tick, bool firstPredict, bool old, bool @new, bool value)> EventTriggerList { get; } =
  320. new();
  321. [Dependency] private readonly IGameTiming _gameTiming = default!;
  322. public override void Initialize()
  323. {
  324. base.Initialize();
  325. SubscribeAllEvent<SetFooMessage>(HandleMessage);
  326. }
  327. private void HandleMessage(SetFooMessage message, EntitySessionEventArgs args)
  328. {
  329. var uid = GetEntity(message.Uid);
  330. var component = EntityManager.GetComponent<PredictionTestComponent>(uid);
  331. var old = component.Foo;
  332. if (Allow)
  333. {
  334. component.Foo = message.NewFoo;
  335. Dirty(uid, component);
  336. }
  337. EventTriggerList.Add((_gameTiming.CurTick, _gameTiming.IsFirstTimePredicted, old, component.Foo, message.NewFoo));
  338. }
  339. }
  340. [Serializable, NetSerializable]
  341. public sealed class SetFooMessage : EntityEventArgs
  342. {
  343. public SetFooMessage(NetEntity uid, bool newFoo)
  344. {
  345. Uid = uid;
  346. NewFoo = newFoo;
  347. }
  348. public NetEntity Uid { get; }
  349. public bool NewFoo { get; }
  350. }
  351. }
  352. // Must be directly located in the namespace or the sourcegen can't find it.
  353. [NetworkedComponent]
  354. [AutoGenerateComponentState]
  355. [RegisterComponent]
  356. public sealed partial class PredictionTestComponent : Component
  357. {
  358. [AutoNetworkedField]
  359. public bool Foo;
  360. }
  361. }