diff --git a/Barotrauma/BarotraumaClient/BarotraumaClient.csproj b/Barotrauma/BarotraumaClient/BarotraumaClient.csproj index b8d454aa4..c3fbd723d 100644 --- a/Barotrauma/BarotraumaClient/BarotraumaClient.csproj +++ b/Barotrauma/BarotraumaClient/BarotraumaClient.csproj @@ -29,6 +29,8 @@ v4.5 0.7.0.1 + + ..\BarotraumaShared\Icon.ico @@ -231,9 +233,15 @@ + + ..\..\Libraries\NuGet\GameAnalytics.Mono.SDK.1.1.12\lib\net45\GameAnalytics.Mono.dll + ..\..\Libraries\NuGet\MonoGame.Framework.WindowsDX.3.6.0.1625\lib\net40\MonoGame.Framework.dll + + ..\..\Libraries\NuGet\NLog.4.3.8\lib\net45\NLog.dll + False @@ -247,9 +255,12 @@ ..\..\Libraries\NuGet\OpenTK.2.0.0\lib\net20\OpenTK.dll + + ..\..\Libraries\NuGet\GameAnalytics.Mono.SDK.1.1.12\lib\net45\System.Data.SQLite.dll + - + ..\..\Libraries\NuGet\RestSharp.105.2.3\lib\net45\RestSharp.dll @@ -324,6 +335,13 @@ + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + @@ -35,7 +35,7 @@ - + @@ -48,7 +48,7 @@ - + @@ -98,7 +98,7 @@ - + @@ -164,7 +164,7 @@ - + @@ -330,7 +330,7 @@ - + diff --git a/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgun.xml b/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgun.xml index c1db65de7..11f441901 100644 --- a/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgun.xml +++ b/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgun.xml @@ -35,8 +35,7 @@ category="Machine" type="Controller" linkable="true" - disableitemusagewhenselected="true" - > + disableitemusagewhenselected="true"> @@ -55,11 +54,34 @@ + + + + + + + + + + + + + + + + + + + + linkable="true"> @@ -69,6 +91,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -94,7 +158,9 @@ - + + + diff --git a/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgunetc2.png b/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgunetc2.png new file mode 100644 index 000000000..1c0d61764 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Content/Items/Weapons/railgunetc2.png differ diff --git a/Barotrauma/BarotraumaShared/Content/Items/Weapons/weapons.xml b/Barotrauma/BarotraumaShared/Content/Items/Weapons/weapons.xml index ec7ea9d76..d9d44e6c9 100644 --- a/Barotrauma/BarotraumaShared/Content/Items/Weapons/weapons.xml +++ b/Barotrauma/BarotraumaShared/Content/Items/Weapons/weapons.xml @@ -43,8 +43,8 @@ holdpos="35,-10" aimpos="35,-10" handle1="-15,-6" handle2="26,7"/> - - + + @@ -78,7 +78,7 @@ aimpos="90,10" handle1="-10,-7"/> - + @@ -128,7 +128,7 @@ aimpos="90,10" handle1="-11,-7"/> - + @@ -300,7 +300,7 @@ - + diff --git a/Barotrauma/BarotraumaShared/Content/Jobs.xml b/Barotrauma/BarotraumaShared/Content/Jobs.xml index 315a1df39..d5e86f074 100644 --- a/Barotrauma/BarotraumaShared/Content/Jobs.xml +++ b/Barotrauma/BarotraumaShared/Content/Jobs.xml @@ -103,10 +103,10 @@ - - - - + + + + @@ -117,4 +117,4 @@ - \ No newline at end of file + diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index 0cbe6be2b..495add3f1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -78,6 +78,10 @@ namespace Barotrauma public EnemyAIController(Character c, string file) : base(c) { targetMemories = new Dictionary(); + outsideSteering = new SteeringManager(this); + insideSteering = new IndoorsSteeringManager(this, false); + steeringManager = outsideSteering; + state = AIState.None; XDocument doc = XMLExtensions.TryLoadXml(file); if (doc == null || doc.Root == null) return; @@ -103,13 +107,6 @@ namespace Barotrauma fleeHealthThreshold = aiElement.GetAttributeFloat("fleehealththreshold", 0.0f); attachToWalls = aiElement.GetAttributeBool("attachtowalls", false); - - outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, false); - - steeringManager = outsideSteering; - - state = AIState.None; } public override void SelectTarget(AITarget target) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs b/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs index 78bfb3145..bc498df98 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs @@ -52,7 +52,7 @@ namespace Barotrauma public float Range { get; private set; } [Serialize(0.0f, false)] - public float DamageRange { get; private set; } + public float DamageRange { get; set; } [Serialize(0.0f, false)] public float Duration { get; private set; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index 8d681550d..dbf3e7e0c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -1948,8 +1948,21 @@ namespace Barotrauma AnimController.Frozen = false; - GameServer.Log(LogName+" has died (Cause of death: "+causeOfDeath+")", ServerLog.MessageType.Attack); - + GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); + if (GameSettings.SendUserStatistics) + { + string characterType = "Unknown"; + if (this == controlled) + characterType = "Player"; + else if (IsRemotePlayer) + characterType = "RemotePlayer"; + else if (AIController is EnemyAIController) + characterType = "Enemy"; + else if (AIController is HumanAIController) + characterType = "AICrew"; + GameAnalyticsSDK.Net.GameAnalytics.AddDesignEvent("Kill:" + characterType + ":" + SpeciesName + ":" + causeOfDeath); + } + if (OnDeath != null) OnDeath(this, causeOfDeath); KillProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs index 3e0ef35fc..4507569c1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs @@ -51,12 +51,6 @@ namespace Barotrauma //the physics body of the limb public PhysicsBody body; - private readonly int refJointIndex; - - private readonly float steerForce; - - private readonly bool doesFlip; - protected readonly Vector2 stepOffset; public Sprite sprite, damagedSprite; @@ -69,8 +63,6 @@ namespace Barotrauma public readonly bool ignoreCollisions; - private float damage, burnt; - private bool isSevered; private float severedFadeOutTimer; @@ -81,16 +73,9 @@ namespace Barotrauma public const float SoundInterval = 0.4f; public readonly Attack attack; + private List damageModifiers; private Direction dir; - - private List wearingItems; - - private Vector2 animTargetPos; - - private float scale; - - private List damageModifiers; public float AttackTimer; @@ -100,17 +85,13 @@ namespace Barotrauma set { isSevered = value; - if (isSevered) - { - damage = 100.0f; - } +#if CLIENT + if (isSevered) damage = 100.0f; +#endif } } - public bool DoesFlip - { - get { return doesFlip; } - } + public bool DoesFlip { get; private set; } public Vector2 WorldPosition { @@ -132,21 +113,12 @@ namespace Barotrauma get { return body.Rotation; } } - public float Scale - { - get { return scale; } - } + public float Scale { get; private set; } //where an animcontroller is trying to pull the limb, only used for debug visualization - public Vector2 AnimTargetPos - { - get { return animTargetPos; } - } + public Vector2 AnimTargetPos { get; private set; } - public float SteerForce - { - get { return steerForce; } - } + public float SteerForce { get; private set; } public float Mass { @@ -166,38 +138,25 @@ namespace Barotrauma set { dir = (value==-1.0f) ? Direction.Left : Direction.Right; } } - public int RefJointIndex - { - get { return refJointIndex; } - } + public int RefJointIndex { get; private set; } public Vector2 StepOffset { get { return stepOffset; } } - public float Burnt - { - get { return burnt; } - protected set { burnt = MathHelper.Clamp(value, 0.0f, 100.0f); } - } - - public List WearingItems - { - get { return wearingItems; } - } - + public List WearingItems { get; private set; } + public Limb (Character character, XElement element, float scale = 1.0f) { this.character = character; - wearingItems = new List(); + WearingItems = new List(); dir = Direction.Right; + DoesFlip = element.GetAttributeBool("flip", false); - doesFlip = element.GetAttributeBool("flip", false); - - this.scale = scale; + Scale = scale; body = new PhysicsBody(element, scale); @@ -217,7 +176,7 @@ namespace Barotrauma body.UserData = this; - refJointIndex = -1; + RefJointIndex = -1; Vector2 pullJointPos = Vector2.Zero; @@ -240,7 +199,7 @@ namespace Barotrauma stepOffset = element.GetAttributeVector2("stepoffset", Vector2.Zero) * scale; stepOffset = ConvertUnits.ToSimUnits(stepOffset); - refJointIndex = element.GetAttributeInt("refjoint", -1); + RefJointIndex = element.GetAttributeInt("refjoint", -1); } else @@ -254,7 +213,7 @@ namespace Barotrauma GameMain.World.AddJoint(pullJoint); - steerForce = element.GetAttributeFloat("steerforce", 0.0f); + SteerForce = element.GetAttributeFloat("steerforce", 0.0f); if (element.Attribute("mouthpos") != null) { @@ -329,12 +288,12 @@ namespace Barotrauma public void MoveToPos(Vector2 pos, float force, bool pullFromCenter=false) { Vector2 pullPos = body.SimPosition; - if (pullJoint!=null && !pullFromCenter) + if (pullJoint != null && !pullFromCenter) { pullPos = pullJoint.WorldAnchorA; } - animTargetPos = pos; + AnimTargetPos = pos; body.MoveToPos(pos, force, pullPos); } @@ -352,7 +311,7 @@ namespace Barotrauma } } - foreach (WearableSprite wearable in wearingItems) + foreach (WearableSprite wearable in WearingItems) { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { @@ -370,54 +329,13 @@ namespace Barotrauma bleedingAmount *= damageModifier.BleedingMultiplier; } -#if CLIENT - if (playSound) - { - string damageSoundType = (damageType == DamageType.Blunt) ? "LimbBlunt" : "LimbSlash"; - - foreach (DamageModifier damageModifier in appliedDamageModifiers) - { - if (!string.IsNullOrWhiteSpace(damageModifier.DamageSound)) - { - damageSoundType = damageModifier.DamageSound; - break; - } - } - - SoundPlayer.PlayDamageSound(damageSoundType, amount, position); - } - - if (character.UseBloodParticles) - { - float bloodParticleAmount = bleedingAmount <= 0.0f ? 0 : (int)Math.Min(amount / 5, 10); - float bloodParticleSize = MathHelper.Clamp(amount / 50.0f, 0.1f, 1.0f); - - for (int i = 0; i < bloodParticleAmount; i++) - { - var blood = GameMain.ParticleManager.CreateParticle(inWater ? "waterblood" : "blood", WorldPosition, Vector2.Zero, 0.0f, character.AnimController.CurrentHull); - if (blood != null) - { - blood.Size *= bloodParticleSize; - } - } - - if (bloodParticleAmount > 0 && character.CurrentHull != null) - { - character.CurrentHull.AddDecal("blood", WorldPosition, MathHelper.Clamp(bloodParticleSize, 0.5f, 1.0f)); - } - } -#endif - - if (damageType == DamageType.Burn) - { - Burnt += amount * 10.0f; - } - - damage += Math.Max(amount,bleedingAmount) / character.MaxHealth * 100.0f; + AddDamageProjSpecific(position, damageType, amount, bleedingAmount, playSound, appliedDamageModifiers); return new AttackResult(amount, bleedingAmount, appliedDamageModifiers); } + partial void AddDamageProjSpecific(Vector2 position, DamageType damageType, float amount, float bleedingAmount, bool playSound, List appliedDamageModifiers); + public bool SectorHit(Vector2 armorSector, Vector2 simPosition) { if (armorSector == Vector2.Zero) return false; @@ -435,10 +353,7 @@ namespace Barotrauma public void Update(float deltaTime) { - UpdateProjSpecific(); - - if (!character.IsDead) damage = Math.Max(0.0f, damage - deltaTime * 0.1f); - if (burnt > 0.0f) Burnt -= deltaTime; + UpdateProjSpecific(deltaTime); if (LinearVelocity.X > 500.0f) { @@ -463,17 +378,11 @@ namespace Barotrauma if (character.IsDead) return; - damage = Math.Max(0.0f, damage - deltaTime * 0.1f); SoundTimer -= deltaTime; } - partial void UpdateProjSpecific(); - - public void ActivateDamagedSprite() - { - damage = 100.0f; - } - + partial void UpdateProjSpecific(float deltaTime); + public void UpdateAttack(float deltaTime, Vector2 attackPosition, IDamageable damageTarget) { float dist = ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackPosition)); diff --git a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs index f84fa55e1..ffb676918 100644 --- a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs @@ -88,6 +88,7 @@ namespace Barotrauma public void Execute(string[] args) { + if (onExecute == null) return; onExecute(args); } @@ -100,6 +101,7 @@ namespace Barotrauma { if (onClientRequestExecute == null) { + if (onExecute == null) return; onExecute(args); } else @@ -247,11 +249,10 @@ namespace Barotrauma }; })); - commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory]: Spawn an item at the position of the cursor, in the inventory of the controlled character or at a random spawnpoint if the last parameter is omitted.", + commands.Add(new Command("spawnitem", "spawnitem [itemname] [cursor/inventory/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", (string[] args) => { - string errorMsg; - SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out errorMsg); + SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg); if (!string.IsNullOrWhiteSpace(errorMsg)) { ThrowError(errorMsg); @@ -260,20 +261,18 @@ namespace Barotrauma null, (Client client, Vector2 cursorWorldPos, string[] args) => { - string errorMsg; - SpawnItem(args, cursorWorldPos, out errorMsg); + SpawnItem(args, cursorWorldPos, client.Character, out string errorMsg); if (!string.IsNullOrWhiteSpace(errorMsg)) { - ThrowError(errorMsg); + GameMain.Server.SendConsoleMessage(errorMsg, client); } - }, + }, () => { List itemNames = new List(); foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { - ItemPrefab itemPrefab = prefab as ItemPrefab; - if (itemPrefab != null) itemNames.Add(itemPrefab.Name); + if (prefab is ItemPrefab itemPrefab) itemNames.Add(itemPrefab.Name); } return new string[][] @@ -283,6 +282,7 @@ namespace Barotrauma }; })); + commands.Add(new Command("disablecrewai", "disablecrewai: Disable the AI of the NPCs in the crew.", (string[] args) => { HumanAIController.DisableCrewAI = true; @@ -849,8 +849,7 @@ namespace Barotrauma { if (args.Length < 2) return; - int id; - int.TryParse(args[0], out id); + int.TryParse(args[0], out int id); var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); if (client == null) { @@ -891,8 +890,7 @@ namespace Barotrauma return; } - int id; - int.TryParse(args[0], out id); + int.TryParse(args[0], out int id); var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); if (client == null) { @@ -1019,11 +1017,10 @@ namespace Barotrauma commands.Add(new Command("kickid", "kickid [id]: Kick the player with the specified client ID out of the server.", (string[] args) => { - if (GameMain.Server == null || args.Length == 0) return; + if (GameMain.NetworkMember == null || args.Length == 0) return; - int id = 0; - int.TryParse(args[0], out id); - var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); + int.TryParse(args[0], out int id); + var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id); if (client == null) { ThrowError("Client id \"" + id + "\" not found."); @@ -1032,7 +1029,7 @@ namespace Barotrauma ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"?", (reason) => { - GameMain.Server.KickPlayer(client.Name, reason); + GameMain.NetworkMember.KickPlayer(client.Name, reason); }); })); @@ -1073,17 +1070,16 @@ namespace Barotrauma commands.Add(new Command("banid", "banid [id]: Kick and ban the player with the specified client ID from the server.", (string[] args) => { - if (GameMain.Server == null || args.Length == 0) return; + if (GameMain.NetworkMember == null || args.Length == 0) return; - int id = 0; - int.TryParse(args[0], out id); - var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); + int.TryParse(args[0], out int id); + var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id); if (client == null) { ThrowError("Client id \"" + id + "\" not found."); return; } - + ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"?", (reason) => { ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => @@ -1091,8 +1087,7 @@ namespace Barotrauma TimeSpan? banDuration = null; if (!string.IsNullOrWhiteSpace(duration)) { - TimeSpan parsedBanDuration; - if (!TryParseTimeSpan(duration, out parsedBanDuration)) + if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration)) { ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\"."); return; @@ -1100,7 +1095,7 @@ namespace Barotrauma banDuration = parsedBanDuration; } - GameMain.Server.BanPlayer(client.Name, reason, false, banDuration); + GameMain.NetworkMember.BanPlayer(client.Name, reason, false, banDuration); }); }); })); @@ -1117,8 +1112,7 @@ namespace Barotrauma TimeSpan? banDuration = null; if (!string.IsNullOrWhiteSpace(duration)) { - TimeSpan parsedBanDuration; - if (!TryParseTimeSpan(duration, out parsedBanDuration)) + if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration)) { ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\"."); return; @@ -1139,23 +1133,72 @@ namespace Barotrauma } } }); + }); + }, + (string[] args) => + { +#if CLIENT + if (GameMain.Client == null || args.Length == 0) return; + ShowQuestionPrompt("Reason for banning the ip \"" + args[0] + "\"?", (reason) => + { + ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => + { + TimeSpan? banDuration = null; + if (!string.IsNullOrWhiteSpace(duration)) + { + if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration)) + { + ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\"."); + return; + } + banDuration = parsedBanDuration; + } + + GameMain.Client.SendConsoleCommand( + "banip " + + args[0] + " " + + (banDuration.HasValue ? banDuration.Value.TotalSeconds.ToString() : "0") + " " + + reason); + }); }); - +#endif + }, + (Client client, Vector2 cursorPos, string[] args) => + { + if (args.Length < 1) return; + var clients = GameMain.Server.ConnectedClients.FindAll(c => c.Connection.RemoteEndPoint.Address.ToString() == args[0]); + TimeSpan? duration = null; + if (args.Length > 1) + { + if (double.TryParse(args[1], out double durationSeconds)) + { + if (durationSeconds > 0) duration = TimeSpan.FromSeconds(durationSeconds); + } + else + { + GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid ban duration.", client); + return; + } + } + string reason = ""; + if (args.Length > 2) reason = string.Join(" ", args.Skip(2)); + + if (clients.Count == 0) + { + GameMain.Server.BanList.BanPlayer("Unnamed", args[0], reason, duration); + } + else + { + foreach (Client cl in clients) + { + GameMain.Server.BanClient(cl, reason, false, duration); + } + } })); commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", (string[] args) => { - Character tpCharacter = null; - - if (args.Length == 0) - { - tpCharacter = Character.Controlled; - } - else - { - tpCharacter = FindMatchingCharacter(args, false); - } - + Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false); if (tpCharacter == null) return; var cam = GameMain.GameScreen.Cam; @@ -1167,17 +1210,7 @@ namespace Barotrauma null, (Client client, Vector2 cursorWorldPos, string[] args) => { - Character tpCharacter = null; - - if (args.Length == 0) - { - tpCharacter = client.Character; - } - else - { - tpCharacter = FindMatchingCharacter(args, false); - } - + Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false); if (tpCharacter == null) return; var cam = GameMain.GameScreen.Cam; @@ -1216,7 +1249,7 @@ namespace Barotrauma Submarine.LockX = !Submarine.LockX; }, null, null)); - commands.Add(new Command("locky", "loxky: Lock vertical movement of the main submarine.", (string[] args) => + commands.Add(new Command("locky", "locky: Lock vertical movement of the main submarine.", (string[] args) => { Submarine.LockY = !Submarine.LockY; }, null, null)); @@ -1234,18 +1267,29 @@ namespace Barotrauma } })); + commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) => + { + if (args.Length == 0) return; + args[0] = args[0].ToLowerInvariant(); + foreach (MapEntity mapEntity in MapEntity.mapEntityList) + { + if (mapEntity.Name.ToLowerInvariant() == args[0]) + { + ThrowError(mapEntity.ID + ": " + mapEntity.Name.ToString()); + } + } + foreach (Character character in Character.CharacterList) + { + if (character.Name.ToLowerInvariant() == args[0] || character.SpeciesName.ToLowerInvariant() == args[0]) + { + ThrowError(character.ID + ": " + character.Name.ToString()); + } + } + })); + commands.Add(new Command("heal", "heal [character name]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed.", (string[] args) => { - Character healedCharacter = null; - if (args.Length == 0) - { - healedCharacter = Character.Controlled; - } - else - { - healedCharacter = FindMatchingCharacter(args); - } - + Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); if (healedCharacter != null) { healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, null); @@ -1257,16 +1301,7 @@ namespace Barotrauma null, (Client client, Vector2 cursorWorldPos, string[] args) => { - Character healedCharacter = null; - if (args.Length == 0) - { - healedCharacter = client.Character; - } - else - { - healedCharacter = FindMatchingCharacter(args); - } - + Character healedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); if (healedCharacter != null) { healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, null); @@ -1285,16 +1320,7 @@ namespace Barotrauma commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) => { - Character revivedCharacter = null; - if (args.Length == 0) - { - revivedCharacter = Character.Controlled; - } - else - { - revivedCharacter = FindMatchingCharacter(args); - } - + Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); if (revivedCharacter == null) return; revivedCharacter.Revive(false); @@ -1313,16 +1339,7 @@ namespace Barotrauma null, (Client client, Vector2 cursorWorldPos, string[] args) => { - Character revivedCharacter = null; - if (args.Length == 0) - { - revivedCharacter = client.Character; - } - else - { - revivedCharacter = FindMatchingCharacter(args); - } - + Character revivedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); if (revivedCharacter == null) return; revivedCharacter.Revive(false); @@ -1358,16 +1375,16 @@ namespace Barotrauma commands.Add(new Command("ragdoll", "ragdoll [character name]: Force-ragdoll the specified character. If the name parameter is omitted, the controlled character will be ragdolled.", (string[] args) => { - Character ragdolledCharacter = null; - if (args.Length == 0) + Character ragdolledCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); + if (ragdolledCharacter != null) { - ragdolledCharacter = Character.Controlled; + ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled; } - else - { - ragdolledCharacter = FindMatchingCharacter(args); - } - + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + Character ragdolledCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); if (ragdolledCharacter != null) { ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled; @@ -1477,20 +1494,14 @@ namespace Barotrauma commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => { - Character killedCharacter = null; - if (args.Length == 0) - { - killedCharacter = Character.Controlled; - } - else - { - killedCharacter = FindMatchingCharacter(args); - } - - if (killedCharacter != null) - { - killedCharacter.AddDamage(CauseOfDeath.Damage, killedCharacter.MaxHealth * 2, null); - } + Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); + killedCharacter?.AddDamage(CauseOfDeath.Damage, killedCharacter.MaxHealth * 2, null); + }, + null, + (Client client, Vector2 cursorWorldPos, string[] args) => + { + Character killedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); + killedCharacter?.AddDamage(CauseOfDeath.Damage, killedCharacter.MaxHealth * 2, null); })); commands.Add(new Command("killmonsters", "killmonsters: Immediately kills all AI-controlled enemies in the level.", (string[] args) => @@ -2120,7 +2131,7 @@ namespace Barotrauma } } - private static void SpawnItem(string[] args, Vector2 cursorPos, out string errorMsg) + private static void SpawnItem(string[] args, Vector2 cursorPos, Character controlledCharacter, out string errorMsg) { errorMsg = ""; if (args.Length < 1) return; @@ -2128,23 +2139,48 @@ namespace Barotrauma Vector2? spawnPos = null; Inventory spawnInventory = null; - int extraParams = 0; - switch (args.Last()) + if (args.Length > 1) { - case "cursor": - extraParams = 1; - spawnPos = cursorPos; - break; - case "inventory": - extraParams = 1; - spawnInventory = Character.Controlled == null ? null : Character.Controlled.Inventory; - break; - default: - extraParams = 0; - break; + switch (args[1]) + { + case "cursor": + spawnPos = cursorPos; + break; + case "inventory": + spawnInventory = controlledCharacter?.Inventory; + break; + default: + //Check if last arg matches the name of an in-game player + if (GameMain.Server != null) + { + var client = GameMain.Server.ConnectedClients.Find(c => c.Name.ToLower() == args.Last().ToLower()); + if (client == null) + { + NewMessage("No player found with the name \"" + args.Last() + "\". Spawning item at random location. If the player you want to give the item to has a space in their name, try surrounding their name with quotes (\").", Color.Red); + break; + } + else if (client.Character == null) + { + errorMsg = "The player \"" + args.Last() + "\" is connected, but hasn't spawned yet."; + return; + } + else + { + //If the last arg matches the name of an in-game player, set the destination to their inventory. + spawnInventory = client.Character.Inventory; + break; + } + } + else + { + var matchingCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + if (matchingCharacter?.Inventory != null) spawnInventory = matchingCharacter.Inventory; + } + break; + } } - string itemName = string.Join(" ", args.Take(args.Length - extraParams)).ToLowerInvariant(); + string itemName = args[0]; var itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (itemPrefab == null) @@ -2162,7 +2198,6 @@ namespace Barotrauma if (spawnPos != null) { Entity.Spawner.AddToSpawnQueue(itemPrefab, (Vector2)spawnPos); - } else if (spawnInventory != null) { diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs index 7f6c8006c..6f2fd6199 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/CargoMission.cs @@ -13,12 +13,12 @@ namespace Barotrauma private int requiredDeliveryAmount; - public CargoMission(XElement element, Location[] locations) - : base(element, locations) + public CargoMission(MissionPrefab prefab, Location[] locations) + : base(prefab, locations) { - itemConfig = element.Element("Items"); + itemConfig = prefab.ConfigElement.Element("Items"); - requiredDeliveryAmount = element.GetAttributeInt("requireddeliveryamount", 0); + requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0); } private void InitItems() diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs index 3ed0593b2..55133d72a 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/CombatMission.cs @@ -29,6 +29,8 @@ namespace Barotrauma { get { + if (descriptions == null) return ""; + if (GameMain.NetworkMember==null || GameMain.NetworkMember.Character==null) { //non-team-specific description @@ -47,20 +49,20 @@ namespace Barotrauma { if (winner == -1) return ""; - return successMessage + return base.SuccessMessage .Replace("[loser]", teamNames[1 - winner]) .Replace("[winner]", teamNames[winner]); } } - public CombatMission(XElement element, Location[] locations) - : base(element, locations) + public CombatMission(MissionPrefab prefab, Location[] locations) + : base(prefab, locations) { descriptions = new string[] { - element.GetAttributeString("descriptionneutral", ""), - element.GetAttributeString("description1", ""), - element.GetAttributeString("description2", "") + prefab.ConfigElement.GetAttributeString("descriptionneutral", ""), + prefab.ConfigElement.GetAttributeString("description1", ""), + prefab.ConfigElement.GetAttributeString("description2", "") }; for (int i = 0; i < descriptions.Length; i++) @@ -73,8 +75,8 @@ namespace Barotrauma teamNames = new string[] { - element.GetAttributeString("teamname1", "Team A"), - element.GetAttributeString("teamname2", "Team B") + prefab.ConfigElement.GetAttributeString("teamname1", "Team A"), + prefab.ConfigElement.GetAttributeString("teamname2", "Team B") }; } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs index c8d8b2b23..ab981d6f3 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs @@ -1,45 +1,18 @@ using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Xml.Linq; namespace Barotrauma { partial class Mission - { - public static List MissionTypes = new List() { "Random" }; - - private string name; - - private string description; - + { protected bool completed; - protected string successMessage; - protected string failureMessage; + private readonly MissionPrefab prefab; - protected string radarLabel; - - protected List headers; - protected List messages; - - private int reward; - public string Name { - get { return name; } - } - - public virtual string Description - { - get { return description; } - } - - public int Reward - { - get { return reward; } + get { return prefab.Name; } } public bool Completed @@ -48,109 +21,101 @@ namespace Barotrauma set { completed = value; } } + public int Reward + { + get { return prefab.Reward; } + } + public virtual bool AllowRespawn { get { return true; } } - public virtual string RadarLabel - { - get { return radarLabel; } - } - public virtual Vector2 RadarPosition { get { return Vector2.Zero; } } - virtual public string SuccessMessage + public string RadarLabel { - get { return successMessage; } + get { return prefab.RadarLabel; } + } + + public List Headers + { + get; private set; + } + + public List Messages + { + get; private set; + } + + public virtual string SuccessMessage + { + get; + protected set; } public string FailureMessage { - get { return failureMessage; } + get; + protected set; } - public static void Init() + public virtual string Description { - var files = GameMain.SelectedPackage.GetFilesOfType(ContentType.Missions); - foreach (string file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null || doc.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) - { - string missionTypeName = element.Name.ToString(); - missionTypeName = missionTypeName.Replace("Mission", ""); - - if (!MissionTypes.Contains(missionTypeName)) MissionTypes.Add(missionTypeName); - } - - } + get; + protected set; } - public Mission(XElement element, Location[] locations) + public MissionPrefab Prefab { - name = element.GetAttributeString("name", ""); + get { return prefab; } + } + + public Mission(MissionPrefab prefab, Location[] locations) + { + System.Diagnostics.Debug.Assert(locations.Length == 2); - description = element.GetAttributeString("description", ""); + this.prefab = prefab; - reward = element.GetAttributeInt("reward", 1); - - successMessage = element.GetAttributeString("successmessage", - "Mission completed successfully"); - failureMessage = element.GetAttributeString("failuremessage", - "Mission failed"); - - radarLabel = element.GetAttributeString("radarlabel", ""); - - messages = new List(); - headers = new List(); - foreach (XElement subElement in element.Elements()) - { - if (subElement.Name.ToString().ToLowerInvariant() != "message") continue; - headers.Add(subElement.GetAttributeString("header", "")); - messages.Add(subElement.GetAttributeString("text", "")); - } + Description = prefab.Description; + SuccessMessage = prefab.SuccessMessage; + FailureMessage = prefab.FailureMessage; + Headers = new List(prefab.Headers); + Messages = new List(prefab.Messages); for (int n = 0; n < 2; n++) { - description = description.Replace("[location" + (n + 1) + "]", locations[n].Name); - - successMessage = successMessage.Replace("[location" + (n + 1) + "]", locations[n].Name); - failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locations[n].Name); - - for (int m = 0; m < messages.Count; m++) + if (Description != null) Description = Description.Replace("[location" + (n + 1) + "]", locations[n].Name); + if (SuccessMessage != null) SuccessMessage = SuccessMessage.Replace("[location" + (n + 1) + "]", locations[n].Name); + if (FailureMessage != null) FailureMessage = FailureMessage.Replace("[location" + (n + 1) + "]", locations[n].Name); + for (int m = 0; m < Messages.Count; m++) { - messages[m] = messages[m].Replace("[location" + (n + 1) + "]", locations[n].Name); + Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locations[n].Name); } } } + public static Mission LoadRandom(Location[] locations, string seed, string missionType = "", bool isSinglePlayer = false) + { + return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), missionType, isSinglePlayer); + } + public static Mission LoadRandom(Location[] locations, MTRandom rand, string missionType = "", bool isSinglePlayer = false) { + //todo: use something else than strings to define the mission type missionType = missionType.ToLowerInvariant(); - var files = GameMain.SelectedPackage.GetFilesOfType(ContentType.Missions); - string configFile = files[rand.Next(files.Count)]; - - XDocument doc = XMLExtensions.TryLoadXml(configFile); - if (doc == null) return null; - - int eventCount = doc.Root.Elements().Count(); - //int[] commonness = new int[eventCount]; - float[] eventProbability = new float[eventCount]; - - float probabilitySum = 0.0f; - - List matchingElements = new List(); - + List allowedMissions = new List(); if (missionType == "random") { - matchingElements = doc.Root.Elements().ToList(); + allowedMissions.AddRange(MissionPrefab.List); + if (GameMain.Server != null) + { + allowedMissions.RemoveAll(mission => !GameMain.Server.AllowedRandomMissionTypes.Any(a => mission.TypeMatches(a))); + } } else if (missionType == "none") { @@ -158,68 +123,31 @@ namespace Barotrauma } else if (string.IsNullOrWhiteSpace(missionType)) { - matchingElements = doc.Root.Elements().ToList(); + allowedMissions.AddRange(MissionPrefab.List); } else { - matchingElements = doc.Root.Elements().ToList().FindAll(m => m.Name.ToString().ToLowerInvariant().Replace("mission", "") == missionType); + allowedMissions = MissionPrefab.List.FindAll(m => m.TypeMatches(missionType)); } if (isSinglePlayer) { - matchingElements.RemoveAll(m => m.GetAttributeBool("multiplayeronly", false)); + allowedMissions.RemoveAll(m => m.MultiplayerOnly); } else { - matchingElements.RemoveAll(m => m.GetAttributeBool("singleplayeronly", false)); + allowedMissions.RemoveAll(m => m.SingleplayerOnly); } - int i = 0; - foreach (XElement element in matchingElements) - { - eventProbability[i] = element.GetAttributeInt("commonness", 1); - - probabilitySum += eventProbability[i]; - - i++; - } - + float probabilitySum = allowedMissions.Sum(m => m.Commonness); float randomNumber = (float)rand.NextDouble() * probabilitySum; - - i = 0; - foreach (XElement element in matchingElements) + foreach (MissionPrefab missionPrefab in allowedMissions) { - if (randomNumber <= eventProbability[i]) + if (randomNumber <= missionPrefab.Commonness) { - Type t; - string type = element.Name.ToString(); - - try - { - t = Type.GetType("Barotrauma." + type, true, true); - if (t == null) - { - DebugConsole.ThrowError("Error in " + configFile + "! Could not find a mission class of the type \"" + type + "\"."); - continue; - } - } - catch - { - DebugConsole.ThrowError("Error in " + configFile + "! Could not find a mission class of the type \"" + type + "\"."); - continue; - } - - ConstructorInfo constructor = t.GetConstructor(new[] { typeof(XElement), typeof(Location[]) }); - - object instance = constructor.Invoke(new object[] { element, locations }); - - Mission mission = (Mission)instance; - - return mission; + return missionPrefab.Instantiate(locations); } - - randomNumber -= eventProbability[i]; - i++; + randomNumber -= missionPrefab.Commonness; } return null; @@ -251,7 +179,7 @@ namespace Barotrauma var mode = GameMain.GameSession.GameMode as CampaignMode; if (mode == null) return; - mode.Money += reward; + mode.Money += Reward; } } } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs new file mode 100644 index 000000000..169105c9a --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + class MissionPrefab + { + public static List List = new List(); + public static List MissionTypes = new List() { "Random" }; + + private string name; + + public string Name + { + get { return name; } + } + + private Type missionType; + private ConstructorInfo constructor; + + public virtual string Description { get; private set; } + + public bool MultiplayerOnly { get; private set; } + public bool SingleplayerOnly { get; private set; } + + public float Commonness { get; private set; } + + public int Reward { get; private set; } + + public string RadarLabel { get; private set; } + + public List Headers { get; private set; } + public List Messages { get; private set; } + + public string SuccessMessage { get; private set; } + public string FailureMessage { get; private set; } + + public XElement ConfigElement { get; private set; } + + public static void Init() + { + var files = GameMain.SelectedPackage.GetFilesOfType(ContentType.Missions); + foreach (string file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file); + if (doc?.Root == null) continue; + + foreach (XElement element in doc.Root.Elements()) + { + string missionTypeName = element.Name.ToString(); + missionTypeName = missionTypeName.Replace("Mission", ""); + + List.Add(new MissionPrefab(element)); + if (!MissionTypes.Contains(missionTypeName)) MissionTypes.Add(missionTypeName); + } + + } + } + + public MissionPrefab(XElement element) + { + ConfigElement = element; + + name = element.GetAttributeString("name", ""); + Description = element.GetAttributeString("description", ""); + Commonness = element.GetAttributeFloat("commonness", 1.0f); + SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false); + MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); + + Reward = element.GetAttributeInt("reward", 1); + + SuccessMessage = element.GetAttributeString("successmessage", "Mission completed successfully"); + FailureMessage = element.GetAttributeString("failuremessage", "Mission failed"); + RadarLabel = element.GetAttributeString("radarlabel", ""); + + Messages = new List(); + Headers = new List(); + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "message") continue; + Headers.Add(subElement.GetAttributeString("header", "")); + Messages.Add(subElement.GetAttributeString("text", "")); + } + + string type = element.Name.ToString(); + + try + { + missionType = Type.GetType("Barotrauma." + type, true, true); + if (missionType == null) + { + DebugConsole.ThrowError("Error in mission prefab " + Name + "! Could not find a mission class of the type \"" + type + "\"."); + return; + } + } + catch + { + DebugConsole.ThrowError("Error in mission prefab " + Name + "! Could not find a mission class of the type \"" + type + "\"."); + return; + } + constructor = missionType.GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + } + + public Mission Instantiate(Location[] locations) + { + return constructor?.Invoke(new object[] { this, locations }) as Mission; + } + + public bool TypeMatches(string typeName) + { + //TODO: use enums instead of strings? + typeName = typeName.ToLowerInvariant(); + return missionType.Name.ToString().Replace("Mission", "").ToLowerInvariant() == typeName; + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs index bb2b00e00..71259e252 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs @@ -18,10 +18,10 @@ namespace Barotrauma get { return monster != null && !monster.IsDead ? radarPosition : Vector2.Zero; } } - public MonsterMission(XElement element, Location[] locations) - : base(element, locations) + public MonsterMission(MissionPrefab prefab, Location[] locations) + : base(prefab, locations) { - monsterFile = element.GetAttributeString("monsterfile", ""); + monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", ""); } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs index 5ae148b83..b9fc7bb10 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs @@ -1,7 +1,6 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma { @@ -23,10 +22,10 @@ namespace Barotrauma } } - public SalvageMission(XElement element, Location[] locations) - : base(element, locations) + public SalvageMission(MissionPrefab prefab, Location[] locations) + : base(prefab, locations) { - string itemName = element.GetAttributeString("itemname", ""); + string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (itemPrefab == null) @@ -35,10 +34,10 @@ namespace Barotrauma return; } - string spawnPositionTypeStr = element.GetAttributeString("spawntype", ""); + string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || - !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) + !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) { spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } diff --git a/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs new file mode 100644 index 000000000..fa24b69e9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs @@ -0,0 +1,25 @@ +using GameAnalyticsSDK.Net; +using System; + +namespace Barotrauma +{ + public static class GameAnalyticsManager + { + public static void Init() + { +#if DEBUB + GameAnalytics.SetEnabledInfoLog(true); +#endif + GameAnalytics.ConfigureBuild(GameMain.Version.ToString()); + GameAnalytics.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor"); + GameAnalytics.Initialize("a3a073c20982de7c15d21e840e149122", "9010ad9a671233b8d9610d76cec8c897d9ff3ba7"); + + string contentPackageName = GameMain.Config?.SelectedContentPackage?.Name; + if (!string.IsNullOrEmpty(contentPackageName)) + { + GameAnalytics.AddDesignEvent("ContentPackage:" + + contentPackageName.Replace(":", "").Substring(0, Math.Min(32, contentPackageName.Length))); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MissionMode.cs index 77a49fbc3..57fae1264 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MissionMode.cs @@ -16,9 +16,22 @@ : base(preset, param) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - - MTRandom rand = new MTRandom(ToolBox.StringToInt(GameMain.NetLobbyScreen.LevelSeed)); - mission = Mission.LoadRandom(locations, rand, param as string); + if (param is string) + { + mission = Mission.LoadRandom(locations, GameMain.NetLobbyScreen.LevelSeed, (string)param); + } + else if (param is MissionPrefab) + { + mission = ((MissionPrefab)param).Instantiate(locations); + } + else if (param is Mission) + { + mission = (Mission)param; + } + else + { + throw new System.ArgumentException("Unrecognized MissionMode parameter \"" + param + "\""); + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiplayerCampaign.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs similarity index 98% rename from Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiplayerCampaign.cs rename to Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs index ffcbcf961..54155e44a 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiplayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -55,13 +55,7 @@ namespace Barotrauma public override void Start() { - base.Start(); - - if (GameMain.Server != null) - { - CargoManager.CreateItems(); - } - + base.Start(); lastUpdateID++; } diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/TraitorManager.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/TraitorManager.cs index 0e87247bc..b071b3a27 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/TraitorManager.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/TraitorManager.cs @@ -108,10 +108,9 @@ namespace Barotrauma codeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); codeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); - while (traitorCount-- >= 0) + while (traitorCount-- > 0) { - if (traitorCandidates.Count <= 0) - break; + if (traitorCandidates.Count <= 0) break; int traitorIndex = Rand.Int(traitorCandidates.Count); Character traitorCharacter = traitorCandidates[traitorIndex]; diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs index d0ad84284..ef9c4f3fb 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs @@ -91,27 +91,35 @@ namespace Barotrauma set { savePath = value; } } - public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset = null, string missionType = "") + + public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, string missionType = "") + : this(submarine, savePath) + { + GameMode = gameModePreset.Instantiate(missionType); + } + + public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab) + : this(submarine, savePath) + { + GameMode = gameModePreset.Instantiate(missionPrefab); + } + + private GameSession(Submarine submarine, string savePath) { Submarine.MainSub = submarine; - + this.submarine = submarine; GameMain.GameSession = this; - EventManager = new EventManager(this); - this.savePath = savePath; - #if CLIENT CrewManager = new CrewManager(); infoButton = new GUIButton(new Rectangle(10, 10, 100, 20), "Info", "", null); infoButton.OnClicked = ToggleInfoFrame; #endif - - if (gameModePreset != null) GameMode = gameModePreset.Instantiate(missionType); - this.submarine = submarine; } - + + public GameSession(Submarine selectedSub, string saveFile, XDocument doc) : this(selectedSub, saveFile) { @@ -218,8 +226,22 @@ namespace Barotrauma EventManager.StartRound(level); - if (GameMode != null) GameMode.MsgBox(); + if (GameMode != null) + { + GameMode.MsgBox(); + if (GameMode is MultiPlayerCampaign campaign && GameMain.Server != null) + { + campaign.CargoManager.CreateItems(); + } + } + if (GameSettings.SendUserStatistics) + { + GameAnalyticsSDK.Net.GameAnalytics.AddDesignEvent("Submarine:" + submarine.Name); + GameAnalyticsSDK.Net.GameAnalytics.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start, + GameMode.Name, (Mission == null ? "None" : Mission.GetType().ToString())); + } + #if CLIENT roundSummary = new RoundSummary(this); @@ -231,6 +253,11 @@ namespace Barotrauma public void EndRound(string endMessage) { if (Mission != null) Mission.End(); + if (GameSettings.SendUserStatistics) + { + GameAnalyticsSDK.Net.GameAnalytics.AddProgressionEvent((Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail, + GameMode.Name, (Mission == null ? "None" : Mission.GetType().ToString())); + } #if CLIENT if (roundSummary != null) diff --git a/Barotrauma/BarotraumaShared/Source/GameSettings.cs b/Barotrauma/BarotraumaShared/Source/GameSettings.cs index e201d1d40..a476f143f 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSettings.cs @@ -25,7 +25,7 @@ namespace Barotrauma public bool EnableSplashScreen { get; set; } //public bool FullScreenEnabled { get; set; } - + private KeyOrMouse[] keyMapping; private WindowMode windowMode; @@ -51,8 +51,37 @@ namespace Barotrauma } } - private bool unsavedSettings; + private int characterHeadIndex; + public int CharacterHeadIndex + { + get { return characterHeadIndex; } + set + { + if (value == characterHeadIndex) return; + // Begin saving coroutine. Remove any existing save coroutines if one is running. + if (CoroutineManager.IsCoroutineRunning("saveCoroutine")) { CoroutineManager.StopCoroutines("saveCoroutine"); } + CoroutineManager.StartCoroutine(ApplyUnsavedChanges(), "saveCoroutine"); + characterHeadIndex = value; + } + } + + private Gender characterGender; + public Gender CharacterGender + { + get { return characterGender; } + set + { + if (value == characterGender) return; + // Begin saving coroutine. Remove any existing save coroutines if one is running. + if (CoroutineManager.IsCoroutineRunning("saveCoroutine")) { CoroutineManager.StopCoroutines("saveCoroutine"); } + CoroutineManager.StartCoroutine(ApplyUnsavedChanges(), "saveCoroutine"); + + characterGender = value; + } + } + + private bool unsavedSettings; public bool UnsavedSettings { get @@ -125,6 +154,18 @@ namespace Barotrauma public static bool VerboseLogging { get; set; } public static bool SaveDebugConsoleLogs { get; set; } + private static bool sendUserStatistics; + public static bool SendUserStatistics + { + get { return sendUserStatistics; } + set + { + sendUserStatistics = value; + GameMain.Config.Save("config.xml"); + } + } + public static bool ShowUserStatisticsPrompt { get; private set; } + public GameSettings(string filePath) { ContentPackage.LoadAll(ContentPackage.Folder); @@ -143,6 +184,14 @@ namespace Barotrauma VerboseLogging = doc.Root.GetAttributeBool("verboselogging", false); SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", false); + if (doc.Root.Attribute("senduserstatistics") == null) + { + ShowUserStatisticsPrompt = true; + } + else + { + sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", true); + } if (doc == null) { @@ -210,18 +259,15 @@ namespace Barotrauma case "keymapping": foreach (XAttribute attribute in subElement.Attributes()) { - InputType inputType; - if (Enum.TryParse(attribute.Name.ToString(), true, out inputType)) + if (Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType)) { - int mouseButton; - if (int.TryParse(attribute.Value.ToString(), out mouseButton)) + if (int.TryParse(attribute.Value.ToString(), out int mouseButton)) { keyMapping[(int)inputType] = new KeyOrMouse(mouseButton); } else { - Keys key; - if (Enum.TryParse(attribute.Value.ToString(), true, out key)) + if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) { keyMapping[(int)inputType] = new KeyOrMouse(key); } @@ -238,6 +284,9 @@ namespace Barotrauma break; case "player": defaultPlayerName = subElement.GetAttributeString("name", ""); + characterHeadIndex = subElement.GetAttributeInt("headindex", Rand.Int(10)); + characterGender = subElement.GetAttributeString("gender", Rand.Range(0.0f, 1.0f) < 0.5f ? "male" : "female") + .ToLowerInvariant() == "male" ? Gender.Male : Gender.Female; break; } } @@ -259,10 +308,8 @@ namespace Barotrauma { case "contentpackage": string path = subElement.GetAttributeString("path", ""); - - + SelectedContentPackage = ContentPackage.list.Find(cp => cp.Path == path); - if (SelectedContentPackage == null) SelectedContentPackage = new ContentPackage(path); break; } @@ -287,7 +334,8 @@ namespace Barotrauma new XAttribute("soundvolume", soundVolume), new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), - new XAttribute("enablesplashscreen", EnableSplashScreen)); + new XAttribute("enablesplashscreen", EnableSplashScreen), + new XAttribute("senduserstatistics", sendUserStatistics)); if (WasGameUpdated) { @@ -344,8 +392,10 @@ namespace Barotrauma gameplay.Add(jobPreferences); doc.Root.Add(gameplay); - var playerElement = new XElement("player"); - playerElement.Add(new XAttribute("name", defaultPlayerName ?? "")); + var playerElement = new XElement("player", + new XAttribute("name", defaultPlayerName ?? ""), + new XAttribute("headindex", characterHeadIndex), + new XAttribute("gender", characterGender)); doc.Root.Add(playerElement); doc.Save(filePath); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs index e9fae6c86..416082a19 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs @@ -179,7 +179,6 @@ namespace Barotrauma.Items.Components return (picker != null); } - public override bool Combine(Item item) { if (!containableItems.Any(x => x.MatchesItem(item))) return false; @@ -195,6 +194,42 @@ namespace Barotrauma.Items.Components return false; } + public void SetContainedItemPositions() + { + Vector2 simPos = item.SimPosition; + Vector2 displayPos = item.Position; + + foreach (Item contained in Inventory.Items) + { + if (contained == null) continue; + + if (contained.body != null) + { + try + { + contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, 0.0f); + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions", e); +#endif + } + } + + contained.Rect = + new Rectangle( + (int)(displayPos.X - contained.Rect.Width / 2.0f), + (int)(displayPos.Y + contained.Rect.Height / 2.0f), + contained.Rect.Width, contained.Rect.Height); + + contained.Submarine = item.Submarine; + contained.CurrentHull = item.CurrentHull; + + contained.SetContainedItemPositions(); + } + } + public override void OnMapLoaded() { if (itemIds == null) return; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs index b99f8ef31..b161ddff9 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs @@ -99,7 +99,9 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (hull1 == null) return; + //check the hull if the item is movable + if (item.body != null) GetHull(); + if (hull1 == null) return; float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : voltage; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs index 3d6e36af4..5ee79b5d7 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs @@ -93,6 +93,26 @@ namespace Barotrauma.Items.Components } } + public override void OnItemLoaded() + { + if (attack != null && attack.DamageRange <= 0.0f && item.body != null) + { + switch (item.body.BodyShape) + { + case PhysicsBody.Shape.Circle: + attack.DamageRange = item.body.radius; + break; + case PhysicsBody.Shape.Capsule: + attack.DamageRange = item.body.height / 2 + item.body.radius; + break; + case PhysicsBody.Shape.Rectangle: + attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); + break; + } + attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); + } + } + public override bool Use(float deltaTime, Character character = null) { if (character != null && !characterUsable) return false; @@ -196,7 +216,6 @@ namespace Barotrauma.Items.Components if (OnProjectileCollision(fixture, normal)) { hitSomething = true; - //Character.Controlled.AnimController.Teleport(point - Character.Controlled.SimPosition, Vector2.Zero); break; } } @@ -281,8 +300,7 @@ namespace Barotrauma.Items.Components Character character = null; if (attack != null) { - var submarine = target.Body.UserData as Submarine; - if (submarine != null) + if (target.Body.UserData is Submarine submarine) { item.Move(-submarine.Position); item.Submarine = submarine; @@ -290,9 +308,8 @@ namespace Barotrauma.Items.Components return true; } - Limb limb = target.Body.UserData as Limb; Structure structure; - if (limb != null) + if (target.Body.UserData is Limb limb) { attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); if (limb.character != null) @@ -350,13 +367,12 @@ namespace Barotrauma.Items.Components { contained.SetTransform(item.SimPosition, contained.body.Rotation); } - //contained.Condition = 0.0f; //Let the freaking .xml handle it jeez } } if (RemoveOnHit) { - Item.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddToRemoveQueue(item); } return true; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs new file mode 100644 index 000000000..457a91f35 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/AdderComponent.cs @@ -0,0 +1,64 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class AdderComponent : ItemComponent + { + //an array to keep track of how long ago a signal was received on both inputs + protected float[] timeSinceReceived; + + protected float[] receivedSignal; + + + //the output is sent if both inputs have received a signal within the timeframe + protected float timeFrame; + + [InGameEditable, Serialize(0.0f, true)] + public float TimeFrame + { + get { return timeFrame; } + set + { + timeFrame = Math.Max(0.0f, value); + } + } + + public AdderComponent(Item item, XElement element) + : base(item, element) + { + timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; + receivedSignal = new float[2]; + IsActive = true; + } + + public override void Update(float deltaTime, Camera cam) + { + bool sendOutput = true; + for (int i = 0; i < timeSinceReceived.Length; i++) + { + if (timeSinceReceived[i] > timeFrame) sendOutput = false; + timeSinceReceived[i] += deltaTime; + } + if (sendOutput) + { + item.SendSignal(0, (receivedSignal[0] + receivedSignal[1]).ToString(), "signal_out", null); + } + } + + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power=0.0f) + { + switch (connection.Name) + { + case "signal_in1": + float.TryParse(signal, out receivedSignal[0]); + timeSinceReceived[0] = 0.0f; + break; + case "signal_in2": + float.TryParse(signal, out receivedSignal[1]); + timeSinceReceived[1] = 0.0f; + break; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs index 68761a18c..eb6331673 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/DelayComponent.cs @@ -10,49 +10,51 @@ namespace Barotrauma.Items.Components { const int SignalQueueSize = 500; - //the output is sent if both inputs have received a signal within the timeframe - private TimeSpan delay; - - private Queue> signalQueue; + private Queue> signalQueue; - [InGameEditable, Serialize(1.0f, true)] + [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f), Serialize(1.0f, true)] public float Delay { - get { return (float)delay.TotalSeconds; } - set - { - float seconds = MathHelper.Clamp(value, 0.0f, 60.0f); - - delay = new TimeSpan(0,0,0,0, (int)(seconds*1000.0f)); - } + get; + set; } - + + [InGameEditable(ToolTip = "Should the component discard previously received signals when a new one is received."), Serialize(false, true)] + public bool ResetWhenSignalReceived + { + get; + set; + } + public DelayComponent(Item item, XElement element) : base (item, element) { - signalQueue = new Queue>(); - + signalQueue = new Queue>(); IsActive = true; } public override void Update(float deltaTime, Camera cam) { - while (signalQueue.Any() && signalQueue.Peek().Item2 + delay <= DateTime.Now) + foreach (var val in signalQueue) + { + val.Second -= deltaTime; + } + + while (signalQueue.Count > 0 && signalQueue.Peek().Second <= 0.0f) { var signalOut = signalQueue.Dequeue(); - - item.SendSignal(0, signalOut.Item1, "signal_out", null); + item.SendSignal(0, signalOut.First, "signal_out", null); } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power=0.0f) + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f) { switch (connection.Name) { case "signal_in": if (signalQueue.Count >= SignalQueueSize) return; - - signalQueue.Enqueue(new Tuple(signal, DateTime.Now)); + if (ResetWhenSignalReceived) signalQueue.Clear(); + signalQueue.Enqueue(Pair.Create(signal, Delay)); break; } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs index a3581bd0f..3015cfb50 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs @@ -49,14 +49,9 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (motionDetected) - { - item.SendSignal(1, output, "state_out", null); - } - else if (!string.IsNullOrWhiteSpace(falseOutput)) - { - item.SendSignal(1, falseOutput, "state_out", null); - } + string signalOut = motionDetected ? output : falseOutput; + + if (!string.IsNullOrEmpty(signalOut)) item.SendSignal(1, signalOut, "state_out", null); updateTimer -= deltaTime; if (updateTimer > 0.0f) return; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Item.cs b/Barotrauma/BarotraumaShared/Source/Items/Item.cs index 21548bfcb..32da8efeb 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Item.cs @@ -315,9 +315,9 @@ namespace Barotrauma public override string ToString() { #if CLIENT - return (GameMain.DebugDraw) ? Name + "(ID: " + ID + ")" : Name; + return (GameMain.DebugDraw) ? Name + " (ID: " + ID + ")" : Name; #elif SERVER - return Name + "(ID: " + ID + ")"; + return Name + " (ID: " + ID + ")"; #endif } @@ -602,7 +602,7 @@ namespace Barotrauma foreach (Item item in ItemList) item.FindHull(); } - public virtual Hull FindHull() + public Hull FindHull() { if (parentInventory != null && parentInventory.Owner != null) { @@ -648,39 +648,9 @@ namespace Barotrauma public void SetContainedItemPositions() { - if (ownInventory == null) return; - - Vector2 simPos = SimPosition; - Vector2 displayPos = Position; - - foreach (Item contained in ownInventory.Items) + foreach (ItemComponent component in components) { - if (contained == null) continue; - - if (contained.body != null) - { - try - { - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, 0.0f); - } - catch (Exception e) - { -#if DEBUG - DebugConsole.ThrowError("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions", e); -#endif - } - } - - contained.Rect = - new Rectangle( - (int)(displayPos.X - contained.Rect.Width / 2.0f), - (int)(displayPos.Y + contained.Rect.Height / 2.0f), - contained.Rect.Width, contained.Rect.Height); - - contained.Submarine = Submarine; - contained.CurrentHull = CurrentHull; - - contained.SetContainedItemPositions(); + (component as ItemContainer)?.SetContainedItemPositions(); } } @@ -804,7 +774,7 @@ namespace Barotrauma private bool IsInWater() { if (CurrentHull == null) return true; - + float surfaceY = CurrentHull.Surface; return CurrentHull.WaterVolume > 0.0f && Position.Y < surfaceY; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs index 02bd404de..a11ce2b95 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs @@ -80,7 +80,7 @@ namespace Barotrauma if (distSqr > displayRangeSqr) continue; //ignore reactors (don't want to blow them up) - if (item.GetComponent() == null) continue; + if (item.GetComponent() != null) continue; float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs index 1b5a52e8a..7348e0aa5 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs @@ -664,19 +664,20 @@ namespace Barotrauma float damageAmount = 0.0f; for (int i = 0; i < SectionCount; i++) { - if (Vector2.DistanceSquared(SectionPosition(i, true), worldPosition) <= attack.DamageRange * attack.DamageRange) + Rectangle sectionRect = sections[i].rect; + sectionRect.Y -= sections[i].rect.Height; + if (MathUtils.CircleIntersectsRectangle(transformedPos, attack.DamageRange, sectionRect)) { damageAmount = attack.GetStructureDamage(deltaTime); AddDamage(i, damageAmount, attacker); - #if CLIENT - GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); + GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); #endif } } #if CLIENT - if (playSound)// && !SectionBodyDisabled(i)) + if (playSound) { string damageSoundType = (attack.DamageType == DamageType.Blunt) ? "StructureBlunt" : "StructureSlash"; SoundPlayer.PlayDamageSound(damageSoundType, damageAmount, worldPosition, tags: Tags); diff --git a/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs index 1e371a465..13fbcd339 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs @@ -490,7 +490,7 @@ namespace Barotrauma return wayPoints[Rand.Int(wayPoints.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))]; } - public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine, bool tryAssignWayPoint = true) + public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) { List subWayPoints = WayPointList.FindAll(wp => wp.Submarine == submarine); @@ -524,29 +524,24 @@ namespace Barotrauma assignedWayPoints[i] = wp; break; } - if (assignedWayPoints[i] != null) continue; - if (tryAssignWayPoint) + //try to assign a spawnpoint that isn't meant for any specific job + var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.assignedJob == null); + if (nonJobSpecificPoints.Any()) { - //try to assign a spawnpoint that isn't meant for any specific job - var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.assignedJob == null); - - if (nonJobSpecificPoints.Any()) - { - assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)]; - } + assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)]; } if (assignedWayPoints[i] != null) continue; - //everything else failed -> just give a random spawnpoint - assignedWayPoints[i] = GetRandom(SpawnType.Human); + //everything else failed -> just give a random spawnpoint inside the sub + assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, true); } - for (int i = 0; i < assignedWayPoints.Length; i++ ) + for (int i = 0; i < assignedWayPoints.Length; i++) { - if (assignedWayPoints[i]==null) + if (assignedWayPoints[i] == null) { DebugConsole.ThrowError("Couldn't find a waypoint for " + crew[i].Name + "!"); assignedWayPoints[i] = WayPointList[0]; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs index fff97a991..80965f69a 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs @@ -29,10 +29,20 @@ namespace Barotrauma.Networking public byte TeamID = 0; - public Character Character; + private Character character; + public Character Character + { + get { return character; } + set + { + character = value; + if (character != null) HasSpawned = true; + } + } public CharacterInfo CharacterInfo; public NetConnection Connection { get; set; } - public bool InGame; + public bool InGame; + public bool HasSpawned; //has the client spawned as a character during the current round public UInt16 LastRecvGeneralUpdate = 0; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs index 44683fc11..6dbd84ce9 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/GameServer.cs @@ -209,6 +209,8 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RandomizeSettings(); started = true; + if (GameSettings.SendUserStatistics) GameAnalyticsSDK.Net.GameAnalytics.AddDesignEvent("GameServer:Start"); + yield return CoroutineStatus.Success; } @@ -778,11 +780,21 @@ namespace Barotrauma.Networking case ClientPermissions.Ban: string bannedName = inc.ReadString().ToLowerInvariant(); string banReason = inc.ReadString(); + bool range = inc.ReadBoolean(); + double durationSeconds = inc.ReadDouble(); + var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == bannedName); if (bannedClient != null) { Log("Client \"" + sender.Name + "\" banned \"" + bannedClient.Name + "\".", ServerLog.MessageType.ServerMessage); - BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? "Banned by " + sender.Name : banReason, false); + if (durationSeconds > 0) + { + BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? "Banned by " + sender.Name : banReason, range, TimeSpan.FromSeconds(durationSeconds)); + } + else + { + BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? "Banned by " + sender.Name : banReason, range); + } } break; case ClientPermissions.EndRound: @@ -993,7 +1005,7 @@ namespace Barotrauma.Networking outmsg.WriteRangedInteger(0, 2, (int)TraitorsEnabled); - outmsg.WriteRangedInteger(0, Mission.MissionTypes.Count - 1, (GameMain.NetLobbyScreen.MissionTypeIndex)); + outmsg.WriteRangedInteger(0, MissionPrefab.MissionTypes.Count - 1, (GameMain.NetLobbyScreen.MissionTypeIndex)); outmsg.Write((byte)GameMain.NetLobbyScreen.SelectedModeIndex); outmsg.Write(GameMain.NetLobbyScreen.LevelSeed); @@ -1213,7 +1225,7 @@ namespace Barotrauma.Networking //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { - GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, Mission.MissionTypes[GameMain.NetLobbyScreen.MissionTypeIndex]); + GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, MissionPrefab.MissionTypes[GameMain.NetLobbyScreen.MissionTypeIndex]); } if (GameMain.GameSession.GameMode.Mission != null && @@ -1341,11 +1353,12 @@ namespace Barotrauma.Networking List characters = new List(); foreach (Client client in ConnectedClients) { - if (client.Character != null) - characters.Add(client.Character); + if (client.Character != null) characters.Add(client.Character); } - var max = (int)Math.Round(characters.Count * 0.2f, 1); - var traitorCount = Math.Max(1, TraitorsEnabled == YesNoMaybe.Maybe ? Rand.Int(max) + 1 : max); + if (Character != null) characters.Add(Character); + + int max = Math.Max(TraitorUseRatio ? (int)Math.Round(characters.Count * TraitorRatio, 1) : 1, 1); + int traitorCount = Rand.Int(max + 1); TraitorManager = new TraitorManager(this, traitorCount); if (TraitorManager.TraitorList.Count > 0) @@ -1357,6 +1370,8 @@ namespace Barotrauma.Networking } } + if (GameSettings.SendUserStatistics) GameAnalyticsSDK.Net.GameAnalytics.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled")); + SendStartMessage(roundStartSeed, Submarine.MainSub, GameMain.GameSession.GameMode.Preset, connectedClients); yield return CoroutineStatus.Running; @@ -1402,6 +1417,8 @@ namespace Barotrauma.Networking msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); msg.Write(selectedMode.Name); + msg.Write((short)(GameMain.GameSession.GameMode?.Mission == null ? + -1 : MissionPrefab.List.IndexOf(GameMain.GameSession.GameMode.Mission.Prefab))); MultiPlayerCampaign campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; @@ -1495,6 +1512,7 @@ namespace Barotrauma.Networking foreach (Client client in connectedClients) { client.Character = null; + client.HasSpawned = false; client.InGame = false; } } @@ -1614,6 +1632,7 @@ namespace Barotrauma.Networking } client.Character = null; + client.HasSpawned = false; client.InGame = false; if (string.IsNullOrWhiteSpace(msg)) msg = client.Name + " has left the server"; @@ -2063,9 +2082,17 @@ namespace Barotrauma.Networking if (jobPrefab != null) jobPreferences.Add(jobPrefab); } - sender.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, sender.Name, gender); - sender.CharacterInfo.HeadSpriteId = headSpriteId; - sender.JobPreferences = jobPreferences; + sender.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, sender.Name, gender) + { + HeadSpriteId = headSpriteId + }; + + //if the client didn't provide job preferences, we'll use the preferences that are randomly assigned in the Client constructor + Debug.Assert(sender.JobPreferences.Count > 0); + if (jobPreferences.Count > 0) + { + sender.JobPreferences = jobPreferences; + } } public void AssignJobs(List unassigned, bool assignHost) @@ -2110,6 +2137,7 @@ namespace Barotrauma.Networking //if any of the players has chosen a job that is Always Allowed, give them that job for (int i = unassigned.Count - 1; i >= 0; i--) { + if (unassigned[i].JobPreferences.Count == 0) continue; if (!unassigned[i].JobPreferences[0].AllowAlways) continue; unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); @@ -2138,47 +2166,49 @@ namespace Barotrauma.Networking } } - //find a suitable job for the rest of the players - foreach (Client c in unassigned) + //attempt to give the clients a job they have in their job preferences + for (int i = unassigned.Count - 1; i >= 0; i--) { - foreach (JobPrefab preferredJob in c.JobPreferences) + foreach (JobPrefab preferredJob in unassigned[i].JobPreferences) { //the maximum number of players that can have this job hasn't been reached yet // -> assign it to the client - if (assignedClientCount[preferredJob] < preferredJob.MaxNumber && c.Karma >= preferredJob.MinKarma) + if (assignedClientCount[preferredJob] < preferredJob.MaxNumber && unassigned[i].Karma >= preferredJob.MinKarma) { - c.AssignedJob = preferredJob; + unassigned[i].AssignedJob = preferredJob; assignedClientCount[preferredJob]++; + unassigned.RemoveAt(i); break; } - //none of the jobs the client prefers are available anymore - else if (preferredJob == c.JobPreferences.Last()) - { - //find all jobs that are still available - var remainingJobs = JobPrefab.List.FindAll(jp => assignedClientCount[preferredJob] < jp.MaxNumber && c.Karma >= jp.MinKarma); + } + } - //all jobs taken, give a random job - if (remainingJobs.Count == 0) - { - DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); - int jobIndex = Rand.Range(0,JobPrefab.List.Count); - int skips = 0; - while (c.Karma < JobPrefab.List[jobIndex].MinKarma) - { - jobIndex++; - skips++; - if (jobIndex >= JobPrefab.List.Count) jobIndex -= JobPrefab.List.Count; - if (skips >= JobPrefab.List.Count) break; - } - c.AssignedJob = JobPrefab.List[jobIndex]; - assignedClientCount[c.AssignedJob]++; - } - else //some jobs still left, choose one of them by random - { - c.AssignedJob = remainingJobs[Rand.Range(0, remainingJobs.Count)]; - assignedClientCount[c.AssignedJob]++; - } + //give random jobs to rest of the clients + foreach (Client c in unassigned) + { + //find all jobs that are still available + var remainingJobs = JobPrefab.List.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma); + + //all jobs taken, give a random job + if (remainingJobs.Count == 0) + { + DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); + int jobIndex = Rand.Range(0, JobPrefab.List.Count); + int skips = 0; + while (c.Karma < JobPrefab.List[jobIndex].MinKarma) + { + jobIndex++; + skips++; + if (jobIndex >= JobPrefab.List.Count) jobIndex -= JobPrefab.List.Count; + if (skips >= JobPrefab.List.Count) break; } + c.AssignedJob = JobPrefab.List[jobIndex]; + assignedClientCount[c.AssignedJob]++; + } + else //some jobs still left, choose one of them by random + { + c.AssignedJob = remainingJobs[Rand.Range(0, remainingJobs.Count)]; + assignedClientCount[c.AssignedJob]++; } } } @@ -2236,7 +2266,11 @@ namespace Barotrauma.Networking Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); log.Save(); } - + + if (GameSettings.SendUserStatistics) + { + GameAnalyticsSDK.Net.GameAnalytics.AddDesignEvent("GameServer:ShutDown"); + } server.Shutdown("The server has been shut down"); } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs b/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs index a148dde48..63af136a5 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/GameServerSettings.cs @@ -254,6 +254,12 @@ namespace Barotrauma.Networking set; } + public List AllowedRandomMissionTypes + { + get; + set; + } + [Serialize(60f, true)] public float AutoBanTime { @@ -287,6 +293,8 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.ToString()); + doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); + #if SERVER doc.Root.SetAttributeValue("password", password); #endif @@ -334,18 +342,22 @@ namespace Barotrauma.Networking #endif subSelectionMode = SelectionMode.Manual; - Enum.TryParse(doc.Root.GetAttributeString("SubSelection", "Manual"), out subSelectionMode); + Enum.TryParse(doc.Root.GetAttributeString("SubSelection", "Manual"), out subSelectionMode); Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote; modeSelectionMode = SelectionMode.Manual; - Enum.TryParse(doc.Root.GetAttributeString("ModeSelection", "Manual"), out modeSelectionMode); + Enum.TryParse(doc.Root.GetAttributeString("ModeSelection", "Manual"), out modeSelectionMode); Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote; var traitorsEnabled = TraitorsEnabled; - Enum.TryParse(doc.Root.GetAttributeString("TraitorsEnabled", "No"), out traitorsEnabled); + Enum.TryParse(doc.Root.GetAttributeString("TraitorsEnabled", "No"), out traitorsEnabled); TraitorsEnabled = traitorsEnabled; GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + AllowedRandomMissionTypes = doc.Root.GetAttributeStringArray( + "AllowedRandomMissionTypes", + MissionPrefab.MissionTypes.ToArray()).ToList(); + if (GameMain.NetLobbyScreen != null #if CLIENT && GameMain.NetLobbyScreen.ServerMessage != null diff --git a/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs index 913795949..f32d873ce 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/RespawnManager.cs @@ -204,8 +204,11 @@ namespace Barotrauma.Networking respawnShuttle.Velocity = Vector2.Zero; - shuttleSteering.AutoPilot = false; - shuttleSteering.MaintainPos = false; + if (shuttleSteering != null) + { + shuttleSteering.AutoPilot = false; + shuttleSteering.MaintainPos = false; + } } private void UpdateTransporting(float deltaTime) @@ -264,7 +267,10 @@ namespace Barotrauma.Networking respawnShuttle.PhysicsBody.FarseerBody.IgnoreCollisionWith(Level.Loaded.TopBarrier); - shuttleSteering.SetDestinationLevelStart(); + if (shuttleSteering != null) + { + shuttleSteering.SetDestinationLevelStart(); + } foreach (Door door in shuttleDoors) { @@ -284,7 +290,7 @@ namespace Barotrauma.Networking if (!CoroutineManager.IsCoroutineRunning("forcepos")) { - if (shuttleSteering.SteeringPath != null && shuttleSteering.SteeringPath.Finished + if ((shuttleSteering?.SteeringPath != null && shuttleSteering.SteeringPath.Finished) || (respawnShuttle.WorldPosition.Y + respawnShuttle.Borders.Y > Level.Loaded.StartPosition.Y - Level.ShaftHeight && Math.Abs(Level.Loaded.StartPosition.X - respawnShuttle.WorldPosition.X) < 1000.0f)) { @@ -323,7 +329,10 @@ namespace Barotrauma.Networking ResetShuttle(); - shuttleSteering.TargetVelocity = Vector2.Zero; + if (shuttleSteering != null) + { + shuttleSteering.TargetVelocity = Vector2.Zero; + } GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning); diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs index 74954b977..8d26d9caf 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs @@ -139,7 +139,7 @@ namespace Barotrauma.Networking } } - string fileName = serverName + "_" + DateTime.Now.ToShortDateString() + "_" + DateTime.Now.ToShortTimeString() + ".txt"; + string fileName = serverName + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH:mm") + ".txt"; var invalidChars = Path.GetInvalidFileNameChars(); foreach (char invalidChar in invalidChars) diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Voting.cs b/Barotrauma/BarotraumaShared/Source/Networking/Voting.cs index 28d430888..c78a1bede 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Voting.cs @@ -111,11 +111,11 @@ namespace Barotrauma #endif break; case VoteType.EndRound: - if (sender.Character == null) return; + if (!sender.HasSpawned) return; sender.SetVote(voteType, inc.ReadBoolean()); - GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.Character != null && c.GetVote(VoteType.EndRound)); - GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.Character != null); + GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound)); + GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned); break; case VoteType.Kick: diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 7c3a4f556..3c33067bd 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,64 @@ +--------------------------------------------------------------------------------------------------------- +v0.8.1.5 +--------------------------------------------------------------------------------------------------------- + +- Added the option to automatically send crash reports and anonymous usage statistics to the developers. +The usage statistics include information such as the selected game mode, selected submarine, causes of +death and mission outcomes. When the game is started for the first time, a message box prompts you to +select whether you want to send the information or not. +- Fixed a bug that caused clients to get desynced when purchasing items in the multiplayer campaign. +- Added a signal component that adds the received signals together. +- Devices outside the submarine can be rewired in-game (not just in the sub editor). +- Fixed a crash caused by vision obstruction logic. +- Fixed clients being unable to give non-permanent or range bans. +- Clients are allowed to vote to end the round if they have spawned at some point during the round, +even if the character they controlled doesn't exist anymore. +- Dedicated servers can give clients the permission to use console commands that aren't available in +for dedicated server (e.g. los, lights, control) +- Banip, banid & kickid commands can be used by clients now (if they have the permission to do so). +- Spawnitem [item] inventory, ragdoll and kill commands target the character of the client using +the command instead of the host's character. +- Spawnitem can be used to spawn items in the inventory of a specific character. +- Fixed explosions with an EMP value only damaging reactors (when they should only ignore reactors). +- Fire can only explode oxygen tanks that are >25% full (otherwise the condition of the tank just drops +to 0). Prevents infinite explosions when an oxygen generator is on fire with oxygen tanks inside. +- Fixed projectiles with a damage range of 0 not applying their structuredamage value to structures. +- Items with a physics body can be used as pumps, so now it's possible to make portable items that remove +water from inside the sub. +- Fixed traitor ratio setting being ignored and the minimum number of traitors being 2. +- Fixed crashes caused by custom characters with no AI configuration. +- Character head and gender settings are saved. + +--------------------------------------------------------------------------------------------------------- +v0.8.1.4 +--------------------------------------------------------------------------------------------------------- + +- Fixed clients getting assigned random jobs regardless of job preferences. + +--------------------------------------------------------------------------------------------------------- +v0.8.1.3 +--------------------------------------------------------------------------------------------------------- + +- Fixed server-side crashes during job assignment if a client hasn't sent any job preferences. +- Fixed crashing if the selected respawn shuttle doesn't have a navigation terminal or any other item +with a Steering component. +- Fixed InWater status effects triggering when an item is fabricated, causing issues such as +water-sensitive items to breaking/exploding immediately after being fabricated. +- Fixed motion sensors sending out signals even if the output is set to nothing. +- Fixed crashing when a round starts if the sub has been saved while a fabricator was running. +- Fixed explosives not detonating inside railgun shells. +- Fixed characters spawning inside the respawn shuttle if no suitable spawnpoint is found inside the +main submarine. +- Fixed a timing bug in the dedicated server which prevented clients from ever timing out, AI characters +from fixing leaks and reactor usage from being logged. +- Changed the format of the server log filenames to make them more easily sortable. +- Increased assistant's skill levels. +- Added oxygenite tanks. +- Added a rearward facing variant of railgun controller. +- Added railgun loader variants with 1 shell capacity. +- Added railgun shell rack. +- Decreased the skill level requirements for fixing docking ports. + --------------------------------------------------------------------------------------------------------- v0.8.1.2 ---------------------------------------------------------------------------------------------------------