commit ccacceb16a184f00ecd384eede64ca9c4fab08a0 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 14:05:59 2019 +0200 NetEntityEventManager checks the length of the event data (and logs an error if it's too long) before checking if there's still room to keep writing events in the packet. Checking the available room first could lead to situations where an excessively large event can't fit to any packet, "soft-locking" the EventManager without any error messages. commit 5ac8259372aa900adc724aa4da1fd81af41ca195 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 13:41:52 2019 +0200 Don't display disabled limbs on sonar (i.e. severed limbs that have "faded out") commit 5f84df73ad86be96f3678c450351b3905e7317a4 Merge: b981f1635 dc429d6c4 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 13:41:16 2019 +0200 Merge branch 'dev' of https://github.com/Regalis11/Barotrauma-development into dev commit b981f163575b2bfc9a83b9925c94eca19b9d4554 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 13:36:19 2019 +0200 Multiplayer campaign fixes: - Server uses a different temp folder to decompress save/sub files into than the clients. Should fix files occasionally getting corrupted and exceptions when trying to read the files when hosting a server from the main executable. - Some additional debug logging. - Use the base names of the adjacent locations as level seeds (i.e. "Vorta" instead of "Vorta Outpost"). The levels should not change when the type (and full name) of the location changes. commit 42c5d18df77fc7acd5873d8e25f20bdd31b1ed76 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 13:31:06 2019 +0200 Don't transfer files through the network when sending them to the owner of the server (i.e. a client hosting directly from the main executable), but simply tell the client where the file is located. commit dc429d6c450f4893fe29c51d3c830527e587a871 Author: Daniel Asteljoki <daniel.asteljoki@gmail.com> Date: Mon Mar 25 13:30:26 2019 +0200 Added labels next to periscopes in Humpback and Dugong commit 789f02a87a2917dd2ae378f136cbe8dd3236c60d Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 13:29:29 2019 +0200 If loading a submarine fails, wait a bit and retry up to 4 times. Fixes loading occasionally failing when running multiple instances of the game from the same directory. commit be9ea3a58832992b6226917117247e1bf1efeff9 Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 11:03:36 2019 +0200 Fixed a bunch of disconnection messages being in an incorrect format & DisconnectUnauthClient not getting the messages from the xml commit c6f744b4d6b3520720010f5cd4f22a25b42bfc8b Author: Joonas Rikkonen <poe.regalis@gmail.com> Date: Mon Mar 25 10:43:10 2019 +0200 Log entity event errors into server logs when verbose logging is enabled
524 lines
21 KiB
C#
524 lines
21 KiB
C#
using Barotrauma.Extensions;
|
|
using Lidgren.Network;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma.Networking
|
|
{
|
|
class ServerEntityEvent : NetEntityEvent
|
|
{
|
|
private IServerSerializable serializable;
|
|
|
|
public bool Sent;
|
|
|
|
#if DEBUG
|
|
public string StackTrace;
|
|
#endif
|
|
|
|
private double createTime;
|
|
public double CreateTime
|
|
{
|
|
get { return createTime; }
|
|
}
|
|
|
|
public ServerEntityEvent(IServerSerializable entity, UInt16 id)
|
|
: base(entity, id)
|
|
{
|
|
serializable = entity;
|
|
|
|
createTime = Timing.TotalTime;
|
|
|
|
#if DEBUG
|
|
StackTrace = Environment.StackTrace.ToString();
|
|
#endif
|
|
}
|
|
|
|
public void Write(NetBuffer msg, Client recipient)
|
|
{
|
|
serializable.ServerWrite(msg, recipient, Data);
|
|
}
|
|
}
|
|
|
|
class ServerEntityEventManager : NetEntityEventManager
|
|
{
|
|
private List<ServerEntityEvent> events;
|
|
|
|
//list of unique events (i.e. !IsDuplicate) created during the round
|
|
//used for syncing clients who join mid-round
|
|
private List<ServerEntityEvent> uniqueEvents;
|
|
|
|
private UInt16 lastSentToAll;
|
|
|
|
public List<ServerEntityEvent> Events
|
|
{
|
|
get { return events; }
|
|
}
|
|
|
|
public List<ServerEntityEvent> UniqueEvents
|
|
{
|
|
get { return uniqueEvents; }
|
|
}
|
|
|
|
private class BufferedEvent
|
|
{
|
|
public readonly Client Sender;
|
|
|
|
public readonly UInt16 CharacterStateID;
|
|
public readonly NetBuffer Data;
|
|
|
|
public readonly Character Character;
|
|
|
|
public readonly IClientSerializable TargetEntity;
|
|
|
|
public bool IsProcessed;
|
|
|
|
public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, NetBuffer data)
|
|
{
|
|
this.Sender = sender;
|
|
this.Character = senderCharacter;
|
|
this.CharacterStateID = characterStateID;
|
|
|
|
this.TargetEntity = targetEntity;
|
|
|
|
this.Data = data;
|
|
}
|
|
}
|
|
|
|
private List<BufferedEvent> bufferedEvents;
|
|
|
|
private UInt16 ID;
|
|
|
|
private GameServer server;
|
|
|
|
public ServerEntityEventManager(GameServer server)
|
|
{
|
|
events = new List<ServerEntityEvent>();
|
|
|
|
this.server = server;
|
|
|
|
bufferedEvents = new List<BufferedEvent>();
|
|
|
|
uniqueEvents = new List<ServerEntityEvent>();
|
|
}
|
|
|
|
public void CreateEvent(IServerSerializable entity, object[] extraData = null)
|
|
{
|
|
if (entity == null || !(entity is Entity))
|
|
{
|
|
DebugConsole.ThrowError("Can't create an entity event for " + entity + "!");
|
|
return;
|
|
}
|
|
|
|
if (((Entity)entity).Removed && !(entity is Level))
|
|
{
|
|
DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace);
|
|
return;
|
|
}
|
|
if (((Entity)entity).IdFreed)
|
|
{
|
|
DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace);
|
|
return;
|
|
}
|
|
|
|
var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
|
|
if (extraData != null) newEvent.SetData(extraData);
|
|
|
|
//remove events that have been sent to all clients, they are redundant now
|
|
//keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
|
|
events.RemoveAll(e => NetIdUtils.IdMoreRecent(lastSentToAll, e.ID));
|
|
|
|
if (server.ConnectedClients.Count(c => c.InGame) == 0 && events.Count > 1)
|
|
{
|
|
events.RemoveRange(0, events.Count - 1);
|
|
}
|
|
|
|
for (int i = events.Count - 1; i >= 0; i--)
|
|
{
|
|
//we already have an identical event that's waiting to be sent
|
|
// -> no need to add a new one
|
|
if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return;
|
|
}
|
|
|
|
ID++;
|
|
|
|
events.Add(newEvent);
|
|
|
|
if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
|
|
{
|
|
//create a copy of the event and give it a new ID
|
|
var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
|
|
uniqueEvent.SetData(extraData);
|
|
|
|
uniqueEvents.Add(uniqueEvent);
|
|
}
|
|
}
|
|
|
|
public void Update(List<Client> clients)
|
|
{
|
|
foreach (BufferedEvent bufferedEvent in bufferedEvents)
|
|
{
|
|
if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead)
|
|
{
|
|
bufferedEvent.IsProcessed = true;
|
|
continue;
|
|
}
|
|
|
|
//delay reading the events until the inputs for the corresponding frame have been processed
|
|
|
|
//UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored)
|
|
//atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored
|
|
if (!bufferedEvent.Character.IsUnconscious &&
|
|
NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender);
|
|
}
|
|
|
|
catch (Exception e)
|
|
{
|
|
string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString();
|
|
if (GameSettings.VerboseLogging)
|
|
{
|
|
string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!";
|
|
GameServer.Log(errorMsg + "\n" + e.StackTrace, ServerLog.MessageType.Error);
|
|
DebugConsole.ThrowError(errorMsg, e);
|
|
}
|
|
GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName,
|
|
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
|
"Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace);
|
|
}
|
|
|
|
bufferedEvent.IsProcessed = true;
|
|
}
|
|
|
|
var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync);
|
|
if (inGameClients.Count > 0)
|
|
{
|
|
lastSentToAll = inGameClients[0].LastRecvEntityEventID;
|
|
if (server.OwnerConnection != null)
|
|
{
|
|
var owner = clients.Find(c => c.Connection == server.OwnerConnection);
|
|
if (owner != null)
|
|
{
|
|
lastSentToAll = owner.LastRecvEntityEventID;
|
|
}
|
|
}
|
|
inGameClients.ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) lastSentToAll = c.LastRecvEntityEventID; });
|
|
|
|
clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); });
|
|
|
|
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
|
|
if (firstEventToResend != null && (Timing.TotalTime - firstEventToResend.CreateTime) > 10.0f)
|
|
{
|
|
//it's been 10 seconds since this event was created, kick everyone that hasn't received it yet, this is way too old
|
|
// UNLESS the event was created when the client was still midround syncing, in which case we'll wait until the timeout
|
|
// runs out before kicking the client
|
|
List<Client> toKick = inGameClients.FindAll(c =>
|
|
NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
|
|
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut));
|
|
toKick.ForEach(c =>
|
|
{
|
|
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
|
|
GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event "
|
|
+ (c.LastRecvEntityEventID + 1).ToString() +
|
|
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago)" +
|
|
" Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
|
|
server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesyncOldEvent");
|
|
}
|
|
);
|
|
}
|
|
|
|
if (events.Count > 0)
|
|
{
|
|
//the client is waiting for an event that we don't have anymore
|
|
//(the ID they're expecting is smaller than the ID of the first event in our list)
|
|
List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent(events[0].ID, (UInt16)(c.LastRecvEntityEventID + 1)));
|
|
toKick.ForEach(c =>
|
|
{
|
|
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
|
|
GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
|
|
server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesyncRemovedEvent");
|
|
});
|
|
}
|
|
}
|
|
|
|
var timedOutClients = clients.FindAll(c => c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
|
|
foreach (Client timedOutClient in timedOutClients)
|
|
{
|
|
GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error);
|
|
GameMain.Server.DisconnectClient(timedOutClient, "", "ServerMessage.SyncTimeout");
|
|
}
|
|
|
|
bufferedEvents.RemoveAll(b => b.IsProcessed);
|
|
}
|
|
|
|
private void BufferEvent(BufferedEvent bufferedEvent)
|
|
{
|
|
if (bufferedEvents.Count > 512)
|
|
{
|
|
//should normally never happen
|
|
|
|
//a client could potentially spam events with a much higher character state ID
|
|
//than the state of their character and/or stop sending character inputs,
|
|
//so we'll drop some events to make sure no-one blows up our buffer
|
|
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...");
|
|
bufferedEvents.RemoveRange(0, 256);
|
|
}
|
|
|
|
bufferedEvents.Add(bufferedEvent);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Writes all the events that the client hasn't received yet into the outgoing message
|
|
/// </summary>
|
|
public void Write(Client client, NetOutgoingMessage msg)
|
|
{
|
|
Write(client, msg, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes all the events that the client hasn't received yet into the outgoing message
|
|
/// </summary>
|
|
public void Write(Client client, NetOutgoingMessage msg, out List<NetEntityEvent> sentEvents)
|
|
{
|
|
List<NetEntityEvent> eventsToSync = null;
|
|
if (client.NeedsMidRoundSync)
|
|
{
|
|
eventsToSync = GetEventsToSync(client, uniqueEvents);
|
|
}
|
|
else
|
|
{
|
|
eventsToSync = GetEventsToSync(client, events);
|
|
}
|
|
|
|
if (eventsToSync.Count == 0)
|
|
{
|
|
sentEvents = eventsToSync;
|
|
return;
|
|
}
|
|
|
|
//too many events for one packet
|
|
if (eventsToSync.Count > 200)
|
|
{
|
|
if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync)
|
|
{
|
|
Color color = eventsToSync.Count > 500 ? Color.Red : Color.Orange;
|
|
if (eventsToSync.Count < 300) { color = Color.Yellow; }
|
|
string warningMsg = "WARNING: event count very high: " + eventsToSync.Count;
|
|
|
|
var sortedEvents = eventsToSync.GroupBy(e => e.Entity.ToString())
|
|
.Select(e => new { Value = e.Key, Count = e.Count() })
|
|
.OrderByDescending(e => e.Count);
|
|
|
|
int count = 1;
|
|
foreach (var sortedEvent in sortedEvents)
|
|
{
|
|
warningMsg += "\n" + count + ". " + (sortedEvent.Value?.ToString() ?? "null") + " x" + sortedEvent.Count;
|
|
count++;
|
|
if (count > 3) { break; }
|
|
}
|
|
if (GameSettings.VerboseLogging)
|
|
{
|
|
GameServer.Log(warningMsg, ServerLog.MessageType.Error);
|
|
}
|
|
DebugConsole.NewMessage(warningMsg, color);
|
|
}
|
|
}
|
|
|
|
if (client.NeedsMidRoundSync)
|
|
{
|
|
msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL);
|
|
msg.Write(client.UnreceivedEntityEventCount);
|
|
msg.Write(client.FirstNewEventID);
|
|
|
|
Write(msg, eventsToSync, out sentEvents, client);
|
|
}
|
|
else
|
|
{
|
|
msg.Write((byte)ServerNetObject.ENTITY_EVENT);
|
|
Write(msg, eventsToSync, out sentEvents, client);
|
|
}
|
|
|
|
foreach (NetEntityEvent entityEvent in sentEvents)
|
|
{
|
|
(entityEvent as ServerEntityEvent).Sent = true;
|
|
client.EntityEventLastSent[entityEvent.ID] = NetTime.Now;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of events that should be sent to the client from the eventList
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
/// <param name="eventList"></param>
|
|
/// <returns></returns>
|
|
private List<NetEntityEvent> GetEventsToSync(Client client, List<ServerEntityEvent> eventList)
|
|
{
|
|
List<NetEntityEvent> eventsToSync = new List<NetEntityEvent>();
|
|
if (eventList.Count == 0) return eventsToSync;
|
|
|
|
//find the index of the first event the client hasn't received
|
|
int startIndex = eventList.Count;
|
|
while (startIndex > 0 &&
|
|
NetIdUtils.IdMoreRecent(eventList[startIndex - 1].ID, client.LastRecvEntityEventID))
|
|
{
|
|
startIndex--;
|
|
}
|
|
|
|
for (int i = startIndex; i < eventList.Count; i++)
|
|
{
|
|
//find the first event that hasn't been sent in roundtriptime or at all
|
|
client.EntityEventLastSent.TryGetValue(eventList[i].ID, out double lastSent);
|
|
|
|
float minInterval = Math.Max(client.Connection.AverageRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2);
|
|
|
|
if (lastSent > NetTime.Now - Math.Min(minInterval, 0.5f))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
eventsToSync.AddRange(eventList.GetRange(i, eventList.Count - i));
|
|
break;
|
|
}
|
|
|
|
return eventsToSync;
|
|
}
|
|
|
|
public void InitClientMidRoundSync(Client client)
|
|
{
|
|
//no need for midround syncing if no events have been created,
|
|
//or if the first created unique event is still in the event list
|
|
if (uniqueEvents.Count == 0 || (events.Count > 0 && events[0].ID == uniqueEvents[0].ID))
|
|
{
|
|
client.UnreceivedEntityEventCount = 0;
|
|
client.FirstNewEventID = 0;
|
|
client.NeedsMidRoundSync = false;
|
|
}
|
|
else
|
|
{
|
|
double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds;
|
|
midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f);
|
|
|
|
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
|
|
client.NeedsMidRoundSync = true;
|
|
client.MidRoundSyncTimeOut = Timing.TotalTime + midRoundSyncTimeOut;
|
|
|
|
//how many (unique) events the clients had missed before joining
|
|
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
|
|
//ID of the first event sent after the client joined
|
|
//(after the client has been synced they'll switch their lastReceivedID
|
|
//to the one before this, and the eventmanagers will start to function "normally")
|
|
client.FirstNewEventID = events.Count == 0 ? (UInt16)0 : events[events.Count - 1].ID;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the events from the message, ignoring ones we've already received
|
|
/// </summary>
|
|
public void Read(NetIncomingMessage msg, Client sender = null)
|
|
{
|
|
UInt16 firstEventID = msg.ReadUInt16();
|
|
int eventCount = msg.ReadByte();
|
|
|
|
for (int i = 0; i < eventCount; i++)
|
|
{
|
|
UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i);
|
|
UInt16 entityID = msg.ReadUInt16();
|
|
|
|
if (entityID == Entity.NullEntityID)
|
|
{
|
|
msg.ReadPadBits();
|
|
if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++;
|
|
continue;
|
|
}
|
|
|
|
byte msgLength = msg.ReadByte();
|
|
|
|
IClientSerializable entity = Entity.FindEntityByID(entityID) as IClientSerializable;
|
|
|
|
//skip the event if we've already received it
|
|
if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1))
|
|
{
|
|
if (GameSettings.VerboseLogging)
|
|
{
|
|
DebugConsole.NewMessage("Received msg " + thisEventID, Color.Red);
|
|
}
|
|
msg.Position += msgLength * 8;
|
|
}
|
|
else if (entity == null)
|
|
{
|
|
//entity not found -> consider the even read and skip over it
|
|
//(can happen, for example, when a client uses a medical item repeatedly
|
|
//and creates an event for it before receiving the event about it being removed)
|
|
if (GameSettings.VerboseLogging)
|
|
{
|
|
DebugConsole.NewMessage(
|
|
"Received msg " + thisEventID + ", entity " + entityID + " not found",
|
|
Microsoft.Xna.Framework.Color.Orange);
|
|
}
|
|
sender.LastSentEntityEventID++;
|
|
msg.Position += msgLength * 8;
|
|
}
|
|
else
|
|
{
|
|
if (GameSettings.VerboseLogging)
|
|
{
|
|
DebugConsole.NewMessage("Received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green);
|
|
}
|
|
|
|
UInt16 characterStateID = msg.ReadUInt16();
|
|
|
|
NetBuffer buffer = new NetBuffer();
|
|
buffer.Write(msg.ReadBytes(msgLength - 2));
|
|
BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer));
|
|
|
|
sender.LastSentEntityEventID++;
|
|
}
|
|
msg.ReadPadBits();
|
|
}
|
|
}
|
|
|
|
protected override void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null)
|
|
{
|
|
var serverEvent = entityEvent as ServerEntityEvent;
|
|
if (serverEvent == null) return;
|
|
|
|
serverEvent.Write(buffer, recipient);
|
|
}
|
|
|
|
protected void ReadEvent(NetBuffer buffer, INetSerializable entity, Client sender = null)
|
|
{
|
|
var clientEntity = entity as IClientSerializable;
|
|
if (clientEntity == null) return;
|
|
|
|
clientEntity.ServerRead(ClientNetObject.ENTITY_STATE, buffer, sender);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
ID = 0;
|
|
events.Clear();
|
|
|
|
bufferedEvents.Clear();
|
|
|
|
lastSentToAll = 0;
|
|
|
|
uniqueEvents.Clear();
|
|
|
|
foreach (Client c in server.ConnectedClients)
|
|
{
|
|
c.EntityEventLastSent.Clear();
|
|
c.LastRecvEntityEventID = 0;
|
|
c.LastSentEntityEventID = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|