using Barotrauma.Extensions; using Barotrauma.Items.Components; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { /// /// How much skills drop towards the job's default skill levels when dying /// public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 20.0f; /// /// How much more the skills drop towards the job's default skill levels /// when dying, in addition to SkillLossPercentageOnDeath, if the player /// chooses to respawn in the middle of the round /// public static float SkillLossPercentageOnImmediateRespawn => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnImmediateRespawn ?? 10.0f; public static bool UseDeathPrompt { get { return GameMain.GameSession?.GameMode is CampaignMode && Level.Loaded != null; } } public enum State { Waiting, Transporting, Returning } private readonly NetworkMember networkMember; private readonly Dictionary> shuttleSteering = new Dictionary>(); private readonly Dictionary> shuttleDoors = new Dictionary>(); private readonly Dictionary> respawnContainers = new Dictionary>(); public class TeamSpecificState { public readonly CharacterTeamType TeamID; public State State; public readonly List RespawnedCharacters = new List(); /// /// When will the shuttle be dispatched with respawned characters /// public DateTime RespawnTime; /// /// When will the sub start heading back out of the level /// public DateTime ReturnTime; public DateTime DespawnTime; public bool RespawnCountdownStarted; public bool ReturnCountdownStarted; public int PendingRespawnCount, RequiredRespawnCount; public int PrevPendingRespawnCount, PrevRequiredRespawnCount; public State CurrentState; //items created during respawn //any respawn items left in the shuttle are removed when the shuttle despawns public readonly List RespawnItems = new List(); public TeamSpecificState(CharacterTeamType teamID) { TeamID = teamID; } } private readonly Dictionary teamSpecificStates = new Dictionary(); public bool UsingShuttle { get { return respawnShuttles.Any(); } } private float maxTransportTime; private float updateReturnTimer; public bool CanRespawnAgain(CharacterTeamType team) { if (teamSpecificStates.TryGetValue(team, out var state)) { return state.CurrentState == State.Transporting && maxTransportTime <= 0.0f; } return false; } private Dictionary respawnShuttles = new Dictionary(); public IEnumerable RespawnShuttles => respawnShuttles.Values; public RespawnManager(NetworkMember networkMember, SubmarineInfo shuttleInfo) : base(null, Entity.RespawnManagerID) { this.networkMember = networkMember; teamSpecificStates = new Dictionary(); int teamCount = GameMain.GameSession?.GameMode is PvPMode ? 2 : 1; if (Level.Loaded == null) { throw new InvalidOperationException("Attempted to instantiate a respawn manager before a level was loaded."); } bool shouldLoadShuttle = shuttleInfo != null && !Level.Loaded.ShouldSpawnCrewInsideOutpost(); respawnShuttles.Clear(); List wifiComponents = new List(); for (int i = 0; i < teamCount; i++) { var teamId = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; teamSpecificStates.Add(teamId, new TeamSpecificState(teamId)); if (shouldLoadShuttle) { shuttleDoors.Add(teamId, new List()); shuttleSteering.Add(teamId, new List()); respawnContainers.Add(teamId, new List()); var respawnShuttle = new Submarine(shuttleInfo, true); if (teamId == CharacterTeamType.Team2) { respawnShuttle.FlipX(); } respawnShuttles.Add(teamId, respawnShuttle); respawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; //set crush depth slightly deeper than the main sub's if (Submarine.MainSub != null) { respawnShuttle.SetCrushDepth(Math.Max(respawnShuttle.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth * 1.2f)); } //prevent wifi components from communicating between the respawn shuttle and other subs foreach (Item item in Item.ItemList) { if (item.Submarine == respawnShuttle) { wifiComponents.AddRange(item.GetComponents()); } } foreach (WifiComponent wifiComponent in wifiComponents) { wifiComponent.TeamID = CharacterTeamType.FriendlyNPC; } ResetShuttle(teamSpecificStates[teamId]); foreach (Item item in Item.ItemList) { if (item.Submarine != respawnShuttle) { continue; } if (item.HasTag(Tags.RespawnContainer)) { if (GameMain.GameSession?.Missions != null) { foreach (var mission in GameMain.GameSession.Missions) { //append the mission type so respawn gear can be configured per mission type (e.g. respawncontainer_kingofthehull) item.AddTag(Tags.RespawnContainer.AppendIfMissing("_" + mission.Prefab.Type)); } } respawnContainers[teamId].Add(item.GetComponent()); } var steering = item.GetComponent(); if (steering != null) { shuttleSteering[teamId].Add(steering); } var door = item.GetComponent(); if (door != null) { shuttleDoors[teamId].Add(door); } //lock all wires to prevent the players from messing up the electronics var connectionPanel = item.GetComponent(); if (connectionPanel != null) { foreach (Connection connection in connectionPanel.Connections) { foreach (Wire wire in connection.Wires) { if (wire != null) { wire.Locked = true; } } } } } } } #if SERVER if (networkMember is GameServer server) { maxTransportTime = server.ServerSettings.MaxTransportTime; } #endif } private bool OnShuttleCollision(Fixture sender, Fixture other, Contact contact) { if (sender.Body.UserData is not Submarine sub || !teamSpecificStates.ContainsKey(sub.TeamID)) { return true; } //ignore collisions with the top barrier when returning return teamSpecificStates[sub.TeamID].CurrentState != State.Returning || other?.Body != Level.Loaded?.TopBarrier; } public void Update(float deltaTime) { foreach (var teamSpecificState in teamSpecificStates.Values) { if (RespawnShuttles.None()) { if (teamSpecificState.CurrentState != State.Waiting) { teamSpecificState.CurrentState = State.Waiting; } } switch (teamSpecificState.CurrentState) { case State.Waiting: UpdateWaiting(teamSpecificState); break; case State.Transporting: UpdateTransporting(teamSpecificState, deltaTime); break; case State.Returning: UpdateReturning(teamSpecificState, deltaTime); break; } } } partial void UpdateWaiting(TeamSpecificState teamSpecificState); private void UpdateTransporting(TeamSpecificState teamSpecificState, float deltaTime) { //infinite transport time -> shuttle wont return if (maxTransportTime <= 0.0f) return; UpdateTransportingProjSpecific(teamSpecificState, deltaTime); } partial void UpdateTransportingProjSpecific(TeamSpecificState teamSpecificState, float deltaTime); public void ForceRespawn() { foreach (var teamSpecificState in teamSpecificStates.Values) { if (teamSpecificState.CurrentState == State.Transporting) { continue; } ResetShuttle(teamSpecificState); teamSpecificState.RespawnCountdownStarted = true; teamSpecificState.RespawnTime = DateTime.Now; teamSpecificState.CurrentState = State.Waiting; } } private void UpdateReturning(TeamSpecificState teamSpecificState, float deltaTime) { updateReturnTimer += deltaTime; if (updateReturnTimer > 1.0f) { updateReturnTimer = 0.0f; shuttleSteering[teamSpecificState.TeamID].ForEach(steering => steering.SetDestinationLevelStart()); UpdateReturningProjSpecific(teamSpecificState, deltaTime); } } partial void UpdateReturningProjSpecific(TeamSpecificState teamSpecificState, float deltaTime); public Submarine GetShuttle(CharacterTeamType team) { if (respawnShuttles.TryGetValue(team, out Submarine sub)) { return sub; } return null; } public TeamSpecificState GetTeamSpecificState(CharacterTeamType team) { if (teamSpecificStates.TryGetValue(team, out TeamSpecificState state)) { return state; } return null; } private void SetShuttleBodyType(CharacterTeamType team, BodyType bodyType) { var shuttle = GetShuttle(team); if (shuttle != null) { shuttle.PhysicsBody.BodyType = bodyType; } } private void ResetShuttle(TeamSpecificState teamSpecificState) { teamSpecificState.ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000)); #if SERVER teamSpecificState.DespawnTime = teamSpecificState.ReturnTime + new TimeSpan(0, 0, seconds: 30); #endif var shuttle = GetShuttle(teamSpecificState.TeamID); if (shuttle == null) { return; } foreach (Item item in Item.ItemList) { if (item.Submarine != shuttle) { continue; } //remove respawn items that have been left in the shuttle if (teamSpecificState.RespawnItems.Contains(item) || respawnContainers[teamSpecificState.TeamID].Any(container => item.IsOwnedBy(container.Item))) { Spawner.AddItemToRemoveQueue(item); continue; } #if CLIENT foreach (var itemComponent in item.Components) { itemComponent.StopLoopingSound(); } #endif //restore other items to full condition and recharge batteries item.Condition = item.MaxCondition; item.GetComponent()?.ResetDeterioration(); var powerContainer = item.GetComponent(); if (powerContainer != null) { powerContainer.Charge = powerContainer.GetCapacity(); } var door = item.GetComponent(); if (door != null) { door.Stuck = 0.0f; } var steering = item.GetComponent(); if (steering != null) { steering.MaintainPos = true; steering.AutoPilot = true; #if SERVER steering.UnsentChanges = true; #endif } } teamSpecificState.RespawnItems.Clear(); foreach (Structure wall in Structure.WallList) { if (wall.Submarine != shuttle) { continue; } for (int i = 0; i < wall.SectionCount; i++) { wall.AddDamage(i, -100000.0f); } } foreach (Hull hull in Hull.HullList) { if (hull.Submarine != shuttle) { continue; } hull.OxygenPercentage = 100.0f; hull.WaterVolume = 0.0f; hull.BallastFlora?.Remove(); } Dictionary characterPositions = new Dictionary(); foreach (Character c in Character.CharacterList) { if (c.Submarine != shuttle) { continue; } if (!teamSpecificState.RespawnedCharacters.Contains(c)) { characterPositions.Add(c, c.WorldPosition); continue; } #if CLIENT if (Character.Controlled == c) { Character.Controlled = null; } #endif c.Kill(CauseOfDeathType.Unknown, null, true); c.Enabled = false; Spawner.AddEntityToRemoveQueue(c); if (c.Inventory != null) { foreach (Item item in c.Inventory.AllItems) { Spawner.AddItemToRemoveQueue(item); } } } shuttle.SetPosition(new Vector2( teamSpecificState.TeamID == CharacterTeamType.Team1 ? Level.Loaded.StartPosition.X : Level.Loaded.EndPosition.X, Level.Loaded.Size.Y + shuttle.Borders.Height)); shuttle.Velocity = Vector2.Zero; foreach (var characterPosition in characterPositions) { characterPosition.Key.TeleportTo(characterPosition.Value); } SetShuttleBodyType(teamSpecificState.TeamID, BodyType.Static); } public static float GetReducedSkill(CharacterInfo characterInfo, Skill skill, float skillLossPercentage, float? currentSkillLevel = null) { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); float currentLevel = currentSkillLevel ?? skill.Level; if (skillPrefab == null) { return currentLevel; } var levelRange = skillPrefab.GetLevelRange(isPvP: GameMain.GameSession?.GameMode is PvPMode); if (currentLevel < levelRange.End) { return currentLevel; } return MathHelper.Lerp(currentLevel, levelRange.End, skillLossPercentage / 100.0f); } partial void RespawnCharactersProjSpecific(Vector2? shuttlePos); public void RespawnCharacters(Vector2? shuttlePos) { RespawnCharactersProjSpecific(shuttlePos); } public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab() { return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); } public static Affliction GetRespawnPenaltyAffliction() { return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f); } public static void GiveRespawnPenaltyAffliction(Character character) { var respawnPenaltyAffliction = GetRespawnPenaltyAffliction(); if (respawnPenaltyAffliction != null) { character.CharacterHealth.ApplyAffliction(targetLimb: null, respawnPenaltyAffliction); } } public Vector2 FindSpawnPos(Submarine respawnShuttle, Submarine mainSub) { if (Level.Loaded == null || Submarine.MainSub == null) { return Vector2.Zero; } Rectangle dockedBorders = respawnShuttle.GetDockedBorders(); Vector2 diffFromDockedBorders = new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2) - new Vector2(respawnShuttle.Borders.Center.X, respawnShuttle.Borders.Y - respawnShuttle.Borders.Height / 2); int minWidth = Math.Max(dockedBorders.Width, 1000); int minHeight = Math.Max(dockedBorders.Height, 1000); List potentialSpawnPositions = FindValidSpawnPoints(respawnShuttle, minWidth, minHeight, minDistFromSubs: 10000.0f, minDistFromCharacters: 5000.0f); if (potentialSpawnPositions.None()) { DebugConsole.NewMessage("Failed to find a shuttle spawn position far away from submarines and characters, attempting to find one closer to to subs and characters..."); potentialSpawnPositions = FindValidSpawnPoints(respawnShuttle, minWidth, minHeight, minDistFromSubs: 1000.0f, minDistFromCharacters: 500.0f); if (potentialSpawnPositions.None()) { DebugConsole.NewMessage("Failed to find a shuttle spawn position, using the level's start position instead."); return Level.Loaded.StartPosition; } } Vector2 bestSpawnPos = Level.Loaded.StartPosition; float bestSpawnPosValue = 0.0f; foreach (var potentialSpawnPos in potentialSpawnPositions) { //the closer the spawnpos is to the main sub, the better float spawnPosValue = 100000.0f / Math.Max(Vector2.Distance(potentialSpawnPos.Position.ToVector2(), mainSub.WorldPosition), 1.0f); //prefer spawnpoints that are at the left side of the sub (so the shuttle doesn't have to go backwards) if (potentialSpawnPos.Position.X > mainSub.WorldPosition.X) { spawnPosValue *= 0.1f; } if (spawnPosValue > bestSpawnPosValue) { bestSpawnPos = potentialSpawnPos.Position.ToVector2(); bestSpawnPosValue = spawnPosValue; } } return bestSpawnPos; } private List FindValidSpawnPoints(Submarine respawnShuttle, float minWidth, float minHeight, float minDistFromSubs, float minDistFromCharacters) { List potentialSpawnPositions = new List(); foreach (Level.InterestingPosition potentialSpawnPos in Level.Loaded.PositionsOfInterest.Where(p => p.PositionType == Level.PositionType.MainPath)) { bool invalid = false; //make sure the shuttle won't overlap with any ruins foreach (var ruin in Level.Loaded.Ruins) { if (Math.Abs(ruin.Area.Center.X - potentialSpawnPos.Position.X) < (minWidth + ruin.Area.Width) / 2) { invalid = true; break; } if (Math.Abs(ruin.Area.Center.Y - potentialSpawnPos.Position.Y) < (minHeight + ruin.Area.Height) / 2) { invalid = true; break; } } if (invalid) { continue; } //make sure there aren't any walls too close var tooCloseCells = Level.Loaded.GetTooCloseCells(potentialSpawnPos.Position.ToVector2(), Math.Max(minWidth, minHeight)); if (tooCloseCells.Any()) { continue; } //make sure the spawnpoint is far enough from other subs foreach (Submarine sub in Submarine.Loaded) { if (sub == respawnShuttle || respawnShuttle.DockedTo.Contains(sub)) { continue; } float minDist = Math.Max(Math.Max(minWidth, minHeight) + Math.Max(sub.Borders.Width, sub.Borders.Height), minDistFromSubs); if (Vector2.DistanceSquared(sub.WorldPosition, potentialSpawnPos.Position.ToVector2()) < minDist * minDist) { invalid = true; break; } } if (invalid) { continue; } foreach (Character character in Character.CharacterList) { if (character.IsDead) { //cannot spawn directly over dead bodies if (Math.Abs(character.WorldPosition.X - potentialSpawnPos.Position.X) < minWidth) { invalid = true; break; } if (Math.Abs(character.WorldPosition.Y - potentialSpawnPos.Position.Y) < minHeight) { invalid = true; break; } } else { //cannot spawn near alive characters (to prevent other players from seeing the shuttle //appear out of nowhere, or monsters from immediatelly wrecking the shuttle) if (Vector2.DistanceSquared(character.WorldPosition, potentialSpawnPos.Position.ToVector2()) < minDistFromCharacters * minDistFromCharacters) { invalid = true; break; } } } if (invalid) { continue; } potentialSpawnPositions.Add(potentialSpawnPos); } return potentialSpawnPositions; } } }