From 1a3c18c727577c00ad38146a4152f34f75f8dfba Mon Sep 17 00:00:00 2001 From: Regalis Date: Mon, 27 Mar 2017 20:44:20 +0300 Subject: [PATCH] EntitySpawner sends spawn/removal messages to clients using EntityEvents. EntityEvents and EntitySpawner used to work independently of each other, with separate IDs, and there was no guarantee that spawning and events would happen in the correct order. For example, a client could fail to read events during midround syncing because the entity has been removed, or read an event for an incorrect entity because the entity has been removed and the ID taken by some other entity. --- Subsurface/Source/Characters/Character.cs | 43 +++-- Subsurface/Source/Characters/HuskInfection.cs | 2 - Subsurface/Source/Characters/Jobs/Job.cs | 5 + Subsurface/Source/DebugConsole.cs | 5 - Subsurface/Source/GameSession/GameSession.cs | 17 +- Subsurface/Source/Items/Item.cs | 6 +- Subsurface/Source/Map/Entity.cs | 2 +- Subsurface/Source/Networking/Client.cs | 7 +- Subsurface/Source/Networking/EntitySpawner.cs | 155 +++++++----------- Subsurface/Source/Networking/GameClient.cs | 11 +- Subsurface/Source/Networking/GameServer.cs | 39 +---- Subsurface/Source/Networking/NetworkMember.cs | 4 +- .../Source/Networking/RespawnManager.cs | 21 +-- 13 files changed, 121 insertions(+), 196 deletions(-) diff --git a/Subsurface/Source/Characters/Character.cs b/Subsurface/Source/Characters/Character.cs index be72137b6..ee7e8e600 100644 --- a/Subsurface/Source/Characters/Character.cs +++ b/Subsurface/Source/Characters/Character.cs @@ -497,17 +497,18 @@ namespace Barotrauma return null; } #endif - + + Character newCharacter = null; if (file != humanConfigFile) { - var enemyCharacter = new AICharacter(file, position, characterInfo, isRemotePlayer); - var ai = new EnemyAIController(enemyCharacter, file); - enemyCharacter.SetAI(ai); + var aiCharacter = new AICharacter(file, position, characterInfo, isRemotePlayer); + var ai = new EnemyAIController(aiCharacter, file); + aiCharacter.SetAI(ai); - enemyCharacter.minHealth = 0.0f; - - return enemyCharacter; + aiCharacter.minHealth = 0.0f; + + newCharacter = aiCharacter; } else if (hasAi) { @@ -517,13 +518,18 @@ namespace Barotrauma aiCharacter.minHealth = -100.0f; - return aiCharacter; + newCharacter = aiCharacter; + } + else + { + newCharacter = new Character(file, position, characterInfo, isRemotePlayer); + newCharacter.minHealth = -100.0f; } - var character = new Character(file, position, characterInfo, isRemotePlayer); - character.minHealth = -100.0f; + if (GameMain.Server != null && Entity.Spawner != null) + Entity.Spawner.CreateNetworkEvent(newCharacter, false); - return character; + return newCharacter; } protected Character(string file, Vector2 position, CharacterInfo characterInfo = null, bool isRemotePlayer = false) @@ -1911,6 +1917,21 @@ namespace Barotrauma if (AnimController != null) AnimController.Remove(); if (Lights.LightManager.ViewTarget == this) Lights.LightManager.ViewTarget = null; + + if (selectedItems[0] != null) selectedItems[0].Drop(this); + if (selectedItems[1] != null) selectedItems[1].Drop(this); + + if (GameMain.GameSession?.CrewManager != null && + GameMain.GameSession.CrewManager.characters.Contains(this)) + { + GameMain.GameSession.CrewManager.characters.Remove(this); + } + + foreach (Character c in CharacterList) + { + if (c.closestCharacter == this) c.closestCharacter = null; + if (c.selectedCharacter == this) c.selectedCharacter = null; + } } } } diff --git a/Subsurface/Source/Characters/HuskInfection.cs b/Subsurface/Source/Characters/HuskInfection.cs index 95c9525d6..9345aaf5d 100644 --- a/Subsurface/Source/Characters/HuskInfection.cs +++ b/Subsurface/Source/Characters/HuskInfection.cs @@ -178,9 +178,7 @@ namespace Barotrauma } character.Enabled = false; - Entity.Spawner.AddToRemoveQueue(character); - Entity.Spawner.AddToSpawnedList(husk); } } } diff --git a/Subsurface/Source/Characters/Jobs/Job.cs b/Subsurface/Source/Characters/Jobs/Job.cs index 642ad7369..3848eddcd 100644 --- a/Subsurface/Source/Characters/Jobs/Job.cs +++ b/Subsurface/Source/Characters/Jobs/Job.cs @@ -106,6 +106,11 @@ namespace Barotrauma } Item item = new Item(itemPrefab, character.Position, null); + + if (GameMain.Server != null && Entity.Spawner != null) + { + Entity.Spawner.CreateNetworkEvent(item, false); + } if (ToolBox.GetAttributeBool(itemElement, "equip", false)) { diff --git a/Subsurface/Source/DebugConsole.cs b/Subsurface/Source/DebugConsole.cs index 2320a6661..c9ad48414 100644 --- a/Subsurface/Source/DebugConsole.cs +++ b/Subsurface/Source/DebugConsole.cs @@ -320,11 +320,6 @@ namespace Barotrauma + "/" + commands[1].ToLower() + ".xml", spawnPosition); } - if (spawnedCharacter != null && GameMain.Server != null) - { - Entity.Spawner.AddToSpawnedList(spawnedCharacter); - } - break; case "spawnitem": if (commands.Length < 2) return; diff --git a/Subsurface/Source/GameSession/GameSession.cs b/Subsurface/Source/GameSession/GameSession.cs index cf5772361..39ef3e848 100644 --- a/Subsurface/Source/GameSession/GameSession.cs +++ b/Subsurface/Source/GameSession/GameSession.cs @@ -109,9 +109,7 @@ namespace Barotrauma CrewManager = new CrewManager(); TaskManager = new TaskManager(this); - - Entity.Spawner.Clear(); - + this.saveFile = saveFile; infoButton = new GUIButton(new Rectangle(10, 10, 100, 20), "Info", GUI.Style, null); @@ -179,7 +177,7 @@ namespace Barotrauma DebugConsole.ThrowError("Couldn't start game session, submarine not selected"); return; } - + if (reloadSub || Submarine.MainSub != submarine) submarine.Load(true); Submarine.MainSub = submarine; if (loadSecondSub) @@ -194,18 +192,13 @@ namespace Barotrauma Submarine.MainSubs[1].Load(false); } } - - //var secondSub = new Submarine(submarine.FilePath, submarine.MD5Hash.Hash); - //secondSub.Load(false); - + if (level != null) { level.Generate(); submarine.SetPosition(submarine.FindSpawnPos(level.StartPosition - new Vector2(0.0f, 2000.0f))); - - //secondSub.SetPosition(level.EndPosition - new Vector2(0.0f, 2000.0f)); - + GameMain.GameScreen.BackgroundCreatureManager.SpawnSprites(80); } @@ -224,6 +217,8 @@ namespace Barotrauma if (gameMode != null) gameMode.MsgBox(); + Entity.Spawner = new EntitySpawner(); + GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f); SoundPlayer.SwitchMusic(); } diff --git a/Subsurface/Source/Items/Item.cs b/Subsurface/Source/Items/Item.cs index 1b96a8b83..95a0abacb 100644 --- a/Subsurface/Source/Items/Item.cs +++ b/Subsurface/Source/Items/Item.cs @@ -944,9 +944,9 @@ namespace Barotrauma else if (body.Enabled) { var holdable = GetComponent(); - if (holdable!=null && holdable.Picker !=null) + if (holdable != null && holdable.Picker?.AnimController != null) { - if (holdable.Picker.SelectedItems[0]==this) + if (holdable.Picker.SelectedItems[0] == this) { depth = holdable.Picker.AnimController.GetLimb(LimbType.RightHand).sprite.Depth + 0.000001f; } @@ -960,7 +960,7 @@ namespace Barotrauma else { body.Draw(spriteBatch, prefab.sprite, color, depth); - } + } } } diff --git a/Subsurface/Source/Map/Entity.cs b/Subsurface/Source/Map/Entity.cs index 8ee834dc8..d811c6ab0 100644 --- a/Subsurface/Source/Map/Entity.cs +++ b/Subsurface/Source/Map/Entity.cs @@ -11,7 +11,7 @@ namespace Barotrauma { private static Dictionary dictionary = new Dictionary(); - public static EntitySpawner Spawner = new EntitySpawner(); + public static EntitySpawner Spawner; private ushort id; diff --git a/Subsurface/Source/Networking/Client.cs b/Subsurface/Source/Networking/Client.cs index c1a463a66..c35e1023c 100644 --- a/Subsurface/Source/Networking/Client.cs +++ b/Subsurface/Source/Networking/Client.cs @@ -38,9 +38,7 @@ namespace Barotrauma.Networking public UInt16 lastSentEntityEventID = 0; public UInt16 lastRecvEntityEventID = 0; - - public UInt16 lastRecvEntitySpawnID = 0; - + public List chatMsgQueue = new List(); public UInt16 lastChatMsgQueueID; public float ChatSpamSpeed; @@ -76,8 +74,7 @@ namespace Barotrauma.Networking lastRecvChatMsgID = ChatMessage.LastID; lastRecvGeneralUpdate = 0; - - lastRecvEntitySpawnID = 0; + lastRecvEntityEventID = 0; UnreceivedEntityEventCount = 0; diff --git a/Subsurface/Source/Networking/EntitySpawner.cs b/Subsurface/Source/Networking/EntitySpawner.cs index 5ee54bc5e..229fe6ab8 100644 --- a/Subsurface/Source/Networking/EntitySpawner.cs +++ b/Subsurface/Source/Networking/EntitySpawner.cs @@ -6,18 +6,12 @@ using System.Linq; namespace Barotrauma { - class EntitySpawner : IServerSerializable + class EntitySpawner : Entity, IServerSerializable { const int MaxEntitiesPerWrite = 10; private enum SpawnableType { Item, Character }; - - public UInt16 NetStateID - { - get; - private set; - } - + interface IEntitySpawnInfo { Entity Spawn(); @@ -84,9 +78,8 @@ namespace Barotrauma } } - private List spawnHistory = new List(); - public EntitySpawner() + : base(null) { spawnQueue = new Queue(); removeQueue = new Queue(); @@ -132,6 +125,13 @@ namespace Barotrauma } } + public void CreateNetworkEvent(Entity entity, bool remove) + { + if (GameMain.Server != null && entity != null) + { + GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); + } + } public void Update() { @@ -142,38 +142,22 @@ namespace Barotrauma var entitySpawnInfo = spawnQueue.Dequeue(); var spawnedEntity = entitySpawnInfo.Spawn(); - if (spawnedEntity != null) AddToSpawnedList(spawnedEntity); + if (spawnedEntity != null) + { + CreateNetworkEvent(spawnedEntity, false); + } } while (removeQueue.Count > 0) { - var entity = removeQueue.Dequeue(); - spawnHistory.Add(new SpawnOrRemove(entity, true)); + var removedEntity = removeQueue.Dequeue(); - entity.Remove(); - NetStateID = (UInt16)spawnHistory.Count; - } - } + if (GameMain.Server != null) + { + CreateNetworkEvent(removedEntity, true); + } - public void AddToSpawnedList(Entity entity) - { - if (GameMain.Server == null) return; - if (entity == null) return; - - spawnHistory.Add(new SpawnOrRemove(entity, false)); - - NetStateID = (UInt16)spawnHistory.Count; - } - - public void AddToSpawnedList(IEnumerable entities) - { - if (GameMain.Server == null) return; - if (entities == null) return; - - foreach (Entity entity in entities) - { - spawnHistory.Add(new SpawnOrRemove(entity, false)); - NetStateID = (UInt16)spawnHistory.Count; + removedEntity.Remove(); } } @@ -181,89 +165,60 @@ namespace Barotrauma { if (GameMain.Server == null) return; - //skip items that the client already knows about - List entities = spawnHistory.Skip((int)client.lastRecvEntitySpawnID).ToList(); + SpawnOrRemove entities = (SpawnOrRemove)extraData[0]; + + message.Write(entities.Remove); - if (entities.Count > MaxEntitiesPerWrite) + if (entities.Remove) { - entities = entities.GetRange(0, MaxEntitiesPerWrite); + message.Write(entities.Entity.ID); } - - message.Write((UInt16)(spawnHistory.IndexOf(entities[0])+1)); - message.WriteRangedInteger(0, MaxEntitiesPerWrite, entities.Count); - - for (int i = 0; i < entities.Count; i++) + else { - message.Write(entities[i].Remove); - - if (entities[i].Remove) + if (entities.Entity is Item) { - message.Write(entities[i].Entity.ID); + message.Write((byte)SpawnableType.Item); + ((Item)entities.Entity).WriteSpawnData(message); } - else + else if (entities.Entity is Character) { - if (entities[i].Entity is Item) - { - message.Write((byte)SpawnableType.Item); - ((Item)entities[i].Entity).WriteSpawnData(message); - } - else if (entities[i].Entity is Character) - { - message.Write((byte)SpawnableType.Character); - ((Character)entities[i].Entity).WriteSpawnData(message); - } + message.Write((byte)SpawnableType.Character); + ((Character)entities.Entity).WriteSpawnData(message); } - } + } } public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer message, float sendingTime) { if (GameMain.Server != null) return; - UInt16 ID = message.ReadUInt16(); - var entityCount = message.ReadRangedInteger(0, MaxEntitiesPerWrite); - for (int i = 0; i < entityCount; i++) + bool remove = message.ReadBoolean(); + + if (remove) { - bool remove = message.ReadBoolean(); + ushort entityId = message.ReadUInt16(); - if (remove) + var entity = FindEntityByID(entityId); + if (entity != null) { - ushort entityId = message.ReadUInt16(); - - var entity = Entity.FindEntityByID(entityId); - if (entity != null && NetIdUtils.IdMoreRecent((UInt16)(ID + i), NetStateID)) - { - entity.Remove(); - } - } - else - { - switch (message.ReadByte()) - { - case (byte)SpawnableType.Item: - Item.ReadSpawnData(message, NetIdUtils.IdMoreRecent((UInt16)(ID + i), NetStateID)); - break; - case (byte)SpawnableType.Character: - Character.ReadSpawnData(message, NetIdUtils.IdMoreRecent((UInt16)(ID + i), NetStateID)); - break; - default: - DebugConsole.ThrowError("Received invalid entity spawn message (unknown spawnable type)"); - break; - } + entity.Remove(); + } + } + else + { + switch (message.ReadByte()) + { + case (byte)SpawnableType.Item: + Item.ReadSpawnData(message, true); + break; + case (byte)SpawnableType.Character: + Character.ReadSpawnData(message, true); + break; + default: + DebugConsole.ThrowError("Received invalid entity spawn message (unknown spawnable type)"); + break; } } - - NetStateID = Math.Max((UInt16)(ID + entityCount - 1), NetStateID); - } - - - public void Clear() - { - NetStateID = 0; - - spawnQueue.Clear(); - removeQueue.Clear(); - spawnHistory.Clear(); } } } diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index 07ed46c29..631a26fec 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -2,11 +2,8 @@ using Lidgren.Network; using Microsoft.Xna.Framework; using System.Collections.Generic; -using FarseerPhysics; using System.IO; -using System.Linq; using System.Text; -using Barotrauma.Items.Components; using System.ComponentModel; namespace Barotrauma.Networking @@ -635,8 +632,7 @@ namespace Barotrauma.Networking //enable spectate button in case we fail to start the round now //(for example, due to a missing sub file or an error) GameMain.NetLobbyScreen.ShowSpectateButton(); - - Entity.Spawner.Clear(); + entityEventManager.Clear(); LastSentEntityEventID = 0; @@ -921,10 +917,6 @@ namespace Barotrauma.Networking case ServerNetObject.CHAT_MESSAGE: ChatMessage.ClientRead(inc); break; - case ServerNetObject.ENTITY_SPAWN: - Item.Spawner.ClientRead(objHeader, inc, sendingTime); - inc.ReadPadBits(); - break; default: DebugConsole.ThrowError("Error while reading update from server (unknown object header \""+objHeader+"\"!)"); break; @@ -959,7 +951,6 @@ namespace Barotrauma.Networking outmsg.Write((byte)ClientNetObject.SYNC_IDS); //outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID); outmsg.Write(ChatMessage.LastID); - outmsg.Write(Entity.Spawner.NetStateID); outmsg.Write(entityEventManager.LastReceivedID); chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, lastSentChatMsgID)); diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index 64dca5769..9931b4029 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -680,11 +680,9 @@ namespace Barotrauma.Networking //TODO: might want to use a clever class for this UInt16 lastRecvChatMsgID = inc.ReadUInt16(); - UInt16 lastRecvEntitySpawnID = inc.ReadUInt16(); UInt16 lastRecvEntityEventID = inc.ReadUInt16(); //last msgs we've created/sent, the client IDs should never be higher than these - UInt16 lastEntitySpawnID = Entity.Spawner.NetStateID; UInt16 lastEntityEventID = entityEventManager.Events.Count == 0 ? (UInt16)0 : entityEventManager.Events.Last().ID; if (c.NeedsMidRoundSync) @@ -705,20 +703,14 @@ namespace Barotrauma.Networking //client thinks they've received a msg we haven't sent yet (corrupted packet, msg read/written incorrectly?) if (NetIdUtils.IdMoreRecent(lastRecvChatMsgID, c.lastChatMsgQueueID)) DebugConsole.ThrowError("client.lastRecvChatMsgID > lastChatMsgQueueID"); - - if (lastRecvEntitySpawnID > lastEntitySpawnID) - DebugConsole.ThrowError("client.lastRecvEntitySpawnID > lastEntitySpawnID"); - + if (lastRecvEntityEventID > lastEntityEventID) DebugConsole.ThrowError("client.lastRecvEntityEventID > lastEntityEventID"); #endif if (NetIdUtils.IdMoreRecent(lastRecvChatMsgID, c.lastRecvChatMsgID)) c.lastRecvChatMsgID = lastRecvChatMsgID; if (NetIdUtils.IdMoreRecent(c.lastRecvChatMsgID, c.lastChatMsgQueueID)) c.lastRecvChatMsgID = c.lastChatMsgQueueID; - - if (NetIdUtils.IdMoreRecent(lastRecvEntitySpawnID, c.lastRecvEntitySpawnID)) c.lastRecvEntitySpawnID = lastRecvEntitySpawnID; - if (NetIdUtils.IdMoreRecent(c.lastRecvEntitySpawnID, lastEntitySpawnID)) c.lastRecvEntitySpawnID = lastEntitySpawnID; - + if (NetIdUtils.IdMoreRecent(lastRecvEntityEventID, c.lastRecvEntityEventID)) c.lastRecvEntityEventID = lastRecvEntityEventID; if (NetIdUtils.IdMoreRecent(c.lastRecvEntityEventID, lastEntityEventID)) c.lastRecvEntityEventID = lastEntityEventID; @@ -839,14 +831,7 @@ namespace Barotrauma.Networking { cMsg.ServerWrite(outmsg, c); } - - if (Item.Spawner.NetStateID > c.lastRecvEntitySpawnID) - { - outmsg.Write((byte)ServerNetObject.ENTITY_SPAWN); - Item.Spawner.ServerWrite(outmsg, c); - outmsg.WritePadBits(); - } - + foreach (Character character in Character.CharacterList) { if (!character.Enabled) continue; @@ -1054,8 +1039,7 @@ namespace Barotrauma.Networking private IEnumerable StartGame(Submarine selectedSub, Submarine selectedShuttle, GameModePreset selectedMode) { initiatedStartGame = true; - - Item.Spawner.Clear(); + entityEventManager.Clear(); GameMain.NetLobbyScreen.StartButton.Enabled = false; @@ -1103,9 +1087,7 @@ namespace Barotrauma.Networking foreach (Client client in teamClients) { client.NeedsMidRoundSync = false; - - client.lastRecvEntitySpawnID = 0; - + client.entityEventLastSent.Clear(); client.lastSentEntityEventID = 0; client.lastRecvEntityEventID = 0; @@ -1149,13 +1131,7 @@ namespace Barotrauma.Networking GameMain.GameSession.CrewManager.characters.Add(myCharacter); } } - - foreach (Character c in GameMain.GameSession.CrewManager.characters) - { - Entity.Spawner.AddToSpawnedList(c); - Entity.Spawner.AddToSpawnedList(c.SpawnItems); - } - + TraitorManager = null; if (TraitorsEnabled == YesNoMaybe.Yes || (TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f)) @@ -1268,8 +1244,7 @@ namespace Barotrauma.Networking myCharacter = null; GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; GameMain.LightManager.LosEnabled = false; - - Item.Spawner.Clear(); + entityEventManager.Clear(); foreach (Client c in connectedClients) { diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs index 06bdf5136..2c9336859 100644 --- a/Subsurface/Source/Networking/NetworkMember.cs +++ b/Subsurface/Source/Networking/NetworkMember.cs @@ -54,9 +54,7 @@ namespace Barotrauma.Networking VOTE, ENTITY_POSITION, ENTITY_EVENT, - ENTITY_EVENT_INITIAL, - - ENTITY_SPAWN + ENTITY_EVENT_INITIAL } enum VoteType diff --git a/Subsurface/Source/Networking/RespawnManager.cs b/Subsurface/Source/Networking/RespawnManager.cs index 5835cc50b..06d1c10b5 100644 --- a/Subsurface/Source/Networking/RespawnManager.cs +++ b/Subsurface/Source/Networking/RespawnManager.cs @@ -428,7 +428,6 @@ namespace Barotrauma.Networking bool myCharacter = i >= clients.Count; var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, !myCharacter, false); - Entity.Spawner.AddToSpawnedList(character); character.TeamID = 1; @@ -448,29 +447,25 @@ namespace Barotrauma.Networking if (divingSuitPrefab != null && oxyPrefab != null) { var divingSuit = new Item(divingSuitPrefab, pos, respawnShuttle); + Entity.Spawner.CreateNetworkEvent(divingSuit, false); + var oxyTank = new Item(oxyPrefab, pos, respawnShuttle); - - divingSuit.Combine(oxyTank); - - spawnedItems.Add(divingSuit); - spawnedItems.Add(oxyTank); + Entity.Spawner.CreateNetworkEvent(oxyTank, false); + divingSuit.Combine(oxyTank); } if (scooterPrefab != null && batteryPrefab != null) { var scooter = new Item(scooterPrefab, pos, respawnShuttle); + Entity.Spawner.CreateNetworkEvent(scooter, false); + var battery = new Item(batteryPrefab, pos, respawnShuttle); + Entity.Spawner.CreateNetworkEvent(battery, false); scooter.Combine(battery); - - spawnedItems.Add(scooter); - spawnedItems.Add(battery); } - + character.GiveJobItems(mainSubSpawnPoints[i]); - Entity.Spawner.AddToSpawnedList(character.SpawnItems); - Entity.Spawner.AddToSpawnedList(spawnedItems); - GameMain.GameSession.CrewManager.characters.Add(character); }