Server LuaCsForBarotrauma
ServerEntityEventManager.cs
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 
7 namespace Barotrauma.Networking
8 {
10  {
11  private IServerSerializable serializable;
12 
13 #if DEBUG
14  public string StackTrace;
15 #endif
16 
17  private double createTime;
18  public double CreateTime
19  {
20  get { return createTime; }
21  }
22 
23  public void ResetCreateTime()
24  {
25  createTime = Timing.TotalTime;
26  }
27 
28  public ServerEntityEvent(IServerSerializable serializableEntity, UInt16 id)
29  : base(serializableEntity, id)
30  {
31  serializable = serializableEntity;
32  createTime = Timing.TotalTime;
33 
34 #if DEBUG
35  StackTrace = Environment.StackTrace.CleanupStackTrace();
36 #endif
37  }
38 
39  public void Write(IWriteMessage msg, Client recipient)
40  {
41  serializable.ServerEventWrite(msg, recipient, Data);
42  }
43  }
44 
46  {
47  private readonly List<ServerEntityEvent> events;
48 
49  //list of unique events (i.e. !IsDuplicate) created during the round
50  //used for syncing clients who join mid-round
51  private readonly List<ServerEntityEvent> uniqueEvents;
52 
53  private UInt16 lastSentToAll;
54  private UInt16 lastSentToAnyone;
55  private double lastSentToAnyoneTime;
56  private double lastWarningTime;
57 
58  public List<ServerEntityEvent> Events
59  {
60  get { return events; }
61  }
62 
63  public List<ServerEntityEvent> UniqueEvents
64  {
65  get { return uniqueEvents; }
66  }
67 
68  private class BufferedEvent
69  {
70  public readonly Client Sender;
71 
72  public readonly UInt16 CharacterStateID;
73  public readonly ReadWriteMessage Data;
74 
75  public readonly Character Character;
76 
77  public readonly IClientSerializable TargetEntity;
78 
79  public bool IsProcessed;
80 
84  public bool RequireCharacter = true;
85 
86  public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, ReadWriteMessage data)
87  {
88  this.Sender = sender;
89  this.Character = senderCharacter;
90  this.CharacterStateID = characterStateID;
91 
92  this.TargetEntity = targetEntity;
93 
94  this.Data = data;
95  }
96  }
97 
98  private readonly List<BufferedEvent> bufferedEvents;
99 
100  private UInt16 ID;
101 
102  private readonly GameServer server;
103 
104  private double lastEventCountHighWarning;
105 
107  {
108  events = new List<ServerEntityEvent>();
109 
110  this.server = server;
111 
112  bufferedEvents = new List<BufferedEvent>();
113 
114  uniqueEvents = new List<ServerEntityEvent>();
115 
116  lastWarningTime = -10.0;
117  }
118 
119  public void CreateEvent(IServerSerializable entity, NetEntityEvent.IData extraData = null)
120  {
121  if (!ValidateEntity(entity)) { return; }
122 
123  var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
124  if (extraData != null) newEvent.SetData(extraData);
125 
126  bool inGameClientsPresent = server.ConnectedClients.Count(c => c.InGame) > 0;
127 
128  //remove old events that have been sent to all clients, they are redundant now
129  // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
130  // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced
131  if (GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
132  {
133  events.RemoveAll(e =>
134  (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) &&
135  e.CreateTime < Timing.TotalTime - NetConfig.EventRemovalTime);
136  }
137 
138  for (int i = events.Count - 1; i >= 0; i--)
139  {
140  //we already have an identical event that's waiting to be sent
141  // -> no need to add a new one
142  if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return;
143  }
144 
145  ID++;
146 
147  events.Add(newEvent);
148 
149  if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
150  {
151  //create a copy of the event and give it a new ID
152  var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
153  uniqueEvent.SetData(extraData);
154 
155  uniqueEvents.Add(uniqueEvent);
156  }
157  }
158 
159  public void Update(List<Client> clients)
160  {
161  foreach (BufferedEvent bufferedEvent in bufferedEvents)
162  {
163  if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead)
164  {
165  if (bufferedEvent.RequireCharacter)
166  {
167  bufferedEvent.IsProcessed = true;
168  continue;
169  }
170  }
171 
172  //delay reading the events until the inputs for the corresponding frame have been processed
173 
174  //UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored)
175  //atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored
176  if (bufferedEvent.Character != null &&
177  !bufferedEvent.Character.IsIncapacitated &&
178  NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
179  {
180  DebugConsole.Log($"Delaying reading entity event sent by a client until the character state has been processed. Event's character state: {bufferedEvent.CharacterStateID}, last processed character state: {bufferedEvent.Character.LastProcessedID}");
181  continue;
182  }
183 
184  try
185  {
186  ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender);
187  }
188 
189  catch (Exception e)
190  {
191  string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString();
192  if (GameSettings.CurrentConfig.VerboseLogging)
193  {
194  string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!";
195  GameServer.Log(errorMsg + "\n" + e.StackTrace.CleanupStackTrace(), ServerLog.MessageType.Error);
196  DebugConsole.ThrowError(errorMsg, e);
197  }
198  GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName,
199  GameAnalyticsManager.ErrorSeverity.Error,
200  "Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace.CleanupStackTrace());
201  }
202 
203  bufferedEvent.IsProcessed = true;
204  }
205 
206  var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync);
207  if (inGameClients.Count > 0)
208  {
209  lastSentToAnyone = inGameClients[0].LastRecvEntityEventID;
210  lastSentToAll = inGameClients[0].LastRecvEntityEventID;
211 
212  if (server.OwnerConnection != null)
213  {
214  var owner = clients.Find(c => c.Connection == server.OwnerConnection);
215  if (owner != null)
216  {
217  lastSentToAll = owner.LastRecvEntityEventID;
218  }
219  }
220  inGameClients.ForEach(c =>
221  {
222  if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) { lastSentToAll = c.LastRecvEntityEventID; }
223  if (NetIdUtils.IdMoreRecent(c.LastRecvEntityEventID, lastSentToAnyone)) { lastSentToAnyone = c.LastRecvEntityEventID; }
224  });
225  lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime;
226 
227  if (Timing.TotalTime - lastWarningTime > 5.0 &&
228  Timing.TotalTime - lastSentToAnyoneTime > 10.0 &&
229  GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration)
230  {
231  lastWarningTime = Timing.TotalTime;
232  GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage);
233  events.ForEach(e => e.ResetCreateTime());
234  //TODO: reset clients if this happens, maybe do it if a majority are behind rather than all of them?
235  }
236 
237  clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); });
238 
239  ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
240  if (firstEventToResend != null &&
241  GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration &&
242  ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime))
243  {
244  // This event is 10 seconds older than the last one we've successfully sent,
245  // kick everyone that hasn't received it yet, this is way too old
246  // UNLESS the event was created when the client was still midround syncing,
247  // in which case we'll wait until the timeout runs out before kicking the client
248  List<Client> toKick = inGameClients.FindAll(c =>
249  NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
250  (firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
251  toKick.ForEach(c =>
252  {
253  DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
254  GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a very old network event ("
255  + (c.LastRecvEntityEventID + 1).ToString() +
256  " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
257  (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" +
258  " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
259  server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent));
260  }
261  );
262  }
263 
264  if (events.Count > 0)
265  {
266  //the client is waiting for an event that we don't have anymore
267  //(the ID they're expecting is smaller than the ID of the first event in our list)
268  List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent(events[0].ID, (UInt16)(c.LastRecvEntityEventID + 1)));
269  toKick.ForEach(c =>
270  {
271  DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
272  GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
273  server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncRemovedEvent));
274  });
275  }
276  }
277 
278  var timedOutClients = clients.FindAll(c => c.Connection != GameMain.Server.OwnerConnection && c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
279  foreach (Client timedOutClient in timedOutClients)
280  {
281  GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error);
282  GameMain.Server.DisconnectClient(timedOutClient, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout));
283  }
284 
285  bufferedEvents.RemoveAll(b => b.IsProcessed);
286  }
287 
288  private void BufferEvent(BufferedEvent bufferedEvent)
289  {
290  if (bufferedEvents.Count > 512)
291  {
292  //should normally never happen
293 
294  //a client could potentially spam events with a much higher character state ID
295  //than the state of their character and/or stop sending character inputs,
296  //so we'll drop some events to make sure no-one blows up our buffer
297  DebugConsole.Log("Excessive amount of events in a client's event buffer. The client may be spamming events or their event IDs might be out of sync. Dropping events...");
298  bufferedEvents.RemoveRange(0, 256);
299  }
300 
301  bufferedEvents.Add(bufferedEvent);
302  }
303 
307  public void Write(in SegmentTableWriter<ServerNetSegment> segmentTable, Client client, IWriteMessage msg)
308  {
309  Write(segmentTable, client, msg, out _);
310  }
311 
315  public void Write(in SegmentTableWriter<ServerNetSegment> segmentTable, Client client, IWriteMessage msg, out List<NetEntityEvent> sentEvents)
316  {
317  List<NetEntityEvent> eventsToSync = GetEventsToSync(client);
318 
319  if (eventsToSync.Count == 0)
320  {
321  sentEvents = eventsToSync;
322  return;
323  }
324 
325  //too many events for one packet
326  //(normal right after a round has just started, don't show a warning if it's been less than 10 seconds)
327  if (eventsToSync.Count > 200 && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0)
328  {
329  if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0)
330  {
331  Color color = eventsToSync.Count > 500 ? Color.Red : Color.Orange;
332  if (eventsToSync.Count < 300) { color = Color.Yellow; }
333  string warningMsg = "WARNING: event count very high: " + eventsToSync.Count;
334 
335  var sortedEvents = eventsToSync.GroupBy(e => e.Entity.ToString())
336  .Select(e => new { Value = e.Key, Count = e.Count() })
337  .OrderByDescending(e => e.Count);
338 
339  int count = 1;
340  foreach (var sortedEvent in sortedEvents)
341  {
342  warningMsg += "\n" + count + ". " + (sortedEvent.Value?.ToString() ?? "null") + " x" + sortedEvent.Count;
343  count++;
344  if (count > 3) { break; }
345  }
346  if (GameSettings.CurrentConfig.VerboseLogging)
347  {
348  GameServer.Log(warningMsg, ServerLog.MessageType.Error);
349  }
350  DebugConsole.NewMessage(warningMsg, color);
351  lastEventCountHighWarning = Timing.TotalTime;
352  }
353  }
354 
355  if (client.NeedsMidRoundSync)
356  {
357  segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial);
359  msg.WriteUInt16(client.FirstNewEventID);
360 
361  Write(msg, eventsToSync, out sentEvents, client);
362  }
363  else
364  {
365  segmentTable.StartNewSegment(ServerNetSegment.EntityEvent);
366  Write(msg, eventsToSync, out sentEvents, client);
367  }
368 
369  foreach (NetEntityEvent entityEvent in sentEvents)
370  {
371  (entityEvent as ServerEntityEvent).Sent = true;
372  client.EntityEventLastSent[entityEvent.ID] = Lidgren.Network.NetTime.Now;
373  }
374  }
375 
379  private List<NetEntityEvent> GetEventsToSync(Client client)
380  {
381  List<NetEntityEvent> eventsToSync = new List<NetEntityEvent>();
382 
383  var eventList = client.NeedsMidRoundSync ? uniqueEvents : events;
384 
385  if (eventList.Count == 0) { return eventsToSync; }
386 
387  //find the index of the first event the client hasn't received
388  int startIndex = eventList.Count;
389  while (startIndex > 0 &&
390  NetIdUtils.IdMoreRecent(eventList[startIndex - 1].ID, client.LastRecvEntityEventID))
391  {
392  startIndex--;
393  }
394 
395  for (int i = startIndex; i < eventList.Count; i++)
396  {
397  //find the first event that hasn't been sent in roundtriptime or at all
398  client.EntityEventLastSent.TryGetValue(eventList[i].ID, out double lastSent);
399 
400  float avgRoundtripTime = 0.01f; //TODO: reimplement client.Connection.AverageRoundtripTime
401  float minInterval = Math.Max(avgRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2);
402 
403  if (lastSent > Lidgren.Network.NetTime.Now - Math.Min(minInterval, 0.5f))
404  {
405  continue;
406  }
407 
408  if (client.NeedsMidRoundSync)
409  {
410  if (i <= client.UnreceivedEntityEventCount)
411  {
412  eventsToSync.AddRange(eventList.GetRange(i, client.UnreceivedEntityEventCount - i));
413  }
414  }
415  else
416  {
417  eventsToSync.AddRange(eventList.GetRange(i, eventList.Count - i));
418  }
419 
420  break;
421  }
422 
423  return eventsToSync;
424  }
425 
426  public void InitClientMidRoundSync(Client client)
427  {
428  //no need for midround syncing if no events have been created,
429  //or if the first created unique event is still in the event list
430  if (uniqueEvents.Count == 0 || (events.Count > 0 && events[0].ID == uniqueEvents[0].ID))
431  {
432  client.UnreceivedEntityEventCount = 0;
433  client.FirstNewEventID = 0;
434  client.NeedsMidRoundSync = false;
435  }
436  else
437  {
438  //assume we can get at least 10 events per second through
439  double midRoundSyncTimeOut = uniqueEvents.Count / 10 * server.UpdateInterval.TotalSeconds;
440  midRoundSyncTimeOut = Math.Max(midRoundSyncTimeOut, server.ServerSettings.MinimumMidRoundSyncTimeout);
441 
442  client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
443  client.NeedsMidRoundSync = true;
444  client.MidRoundSyncTimeOut = Timing.TotalTime + midRoundSyncTimeOut;
445 
446  //how many (unique) events the clients had missed before joining
447  client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
448  //ID of the first event sent after the client joined
449  //(after the client has been synced they'll switch their lastReceivedID
450  //to the one before this, and the eventmanagers will start to function "normally")
451  client.FirstNewEventID = events.Count == 0 ? (UInt16)0 : events[events.Count - 1].ID;
452  }
453  }
454 
458  public void Read(IReadMessage msg, Client sender = null)
459  {
460  msg.ReadPadBits();
461  UInt16 firstEventID = msg.ReadUInt16();
462  int eventCount = msg.ReadByte();
463 
464  for (int i = 0; i < eventCount; i++)
465  {
466  UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i);
467  UInt16 entityID = msg.ReadUInt16();
468 
469  if (entityID == Entity.NullEntityID)
470  {
471  if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++;
472  continue;
473  }
474 
475  int msgLength = (int)msg.ReadVariableUInt32();
476 
478 
479  //skip the event if we've already received it
480  if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1))
481  {
482  if (GameSettings.CurrentConfig.VerboseLogging)
483  {
484  DebugConsole.NewMessage("Received msg " + thisEventID + ", expecting " + sender.LastSentEntityEventID, Color.Red);
485  }
486  msg.BitPosition += msgLength * 8;
487  }
488  else if (entity == null)
489  {
490  //entity not found -> consider the event read and skip over it
491  //(can happen, for example, when a client uses a medical item repeatedly
492  //and creates an event for it before receiving the event about it being removed)
493  if (GameSettings.CurrentConfig.VerboseLogging)
494  {
495  DebugConsole.NewMessage(
496  "Received msg " + thisEventID + ", entity " + entityID + " not found",
497  Microsoft.Xna.Framework.Color.Orange);
498  }
499  sender.LastSentEntityEventID++;
500  msg.BitPosition += msgLength * 8;
501  }
502  else
503  {
504  if (GameSettings.CurrentConfig.VerboseLogging)
505  {
506  DebugConsole.NewMessage("Received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green);
507  }
508 
509  UInt16 characterStateID = msg.ReadUInt16();
510 
511  ReadWriteMessage buffer = new ReadWriteMessage();
512  byte[] temp = msg.ReadBytes(msgLength - 2);
513  buffer.WriteBytes(temp, 0, msgLength - 2);
514  buffer.BitPosition = 0;
515  BufferEvent(
516  new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer)
517  {
518  //hull updates can be sent without a character to allow editing water and fire in spectator mode
519  RequireCharacter = entity is not Hull
520  });
521 
522  sender.LastSentEntityEventID++;
523  }
524  }
525  }
526 
527  protected override void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient = null)
528  {
529  var serverEvent = entityEvent as ServerEntityEvent;
530  if (serverEvent == null) return;
531 
532  serverEvent.Write(buffer, recipient);
533  }
534 
535  protected void ReadEvent(IReadMessage buffer, INetSerializable entity, Client sender = null)
536  {
537  var clientEntity = entity as IClientSerializable;
538  if (clientEntity == null) return;
539 
540  clientEntity.ServerEventRead(buffer, sender);
541  }
542 
543  public void Clear()
544  {
545  ID = 0;
546  events.Clear();
547 
548  bufferedEvents.Clear();
549 
550  lastSentToAll = 0;
551 
552  uniqueEvents.Clear();
553 
554  foreach (Client c in server.ConnectedClients)
555  {
556  c.EntityEventLastSent.Clear();
557  c.LastRecvEntityEventID = 0;
558  c.LastSentEntityEventID = 0;
559  }
560  }
561  }
562 }
const ushort NullEntityID
Definition: Entity.cs:14
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
static GameServer Server
Definition: GameMain.cs:39
static GameSession GameSession
Definition: GameMain.cs:45
readonly Dictionary< UInt16, double > EntityEventLastSent
NetworkConnection OwnerConnection
Definition: GameServer.cs:135
void DisconnectClient(NetworkConnection senderConnection, PeerDisconnectPacket peerDisconnectPacket)
Definition: GameServer.cs:3573
static void Log(string line, ServerLog.MessageType messageType)
Definition: GameServer.cs:4609
static bool ValidateEntity(INetSerializable entity)
ServerEntityEvent(IServerSerializable serializableEntity, UInt16 id)
void Write(IWriteMessage msg, Client recipient)
void Write(in SegmentTableWriter< ServerNetSegment > segmentTable, Client client, IWriteMessage msg)
Writes all the events that the client hasn't received yet into the outgoing message
override void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient=null)
void Read(IReadMessage msg, Client sender=null)
Read the events from the message, ignoring ones we've already received
void Write(in SegmentTableWriter< ServerNetSegment > segmentTable, Client client, IWriteMessage msg, out List< NetEntityEvent > sentEvents)
Writes all the events that the client hasn't received yet into the outgoing message
void ReadEvent(IReadMessage buffer, INetSerializable entity, Client sender=null)
void CreateEvent(IServerSerializable entity, NetEntityEvent.IData extraData=null)
Interface for entities that the clients can send events to the server
void ServerEventRead(IReadMessage msg, Client c)
byte[] ReadBytes(int numberOfBytes)
Interface for entities that the server can send events to the clients
void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData=null)