Merge branch 'master' into patch-2

This commit is contained in:
Blue
2018-07-18 00:25:54 +02:00
committed by GitHub
23 changed files with 355 additions and 198 deletions
@@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.8.1.4")] [assembly: AssemblyVersion("0.8.1.5")]
[assembly: AssemblyFileVersion("0.8.1.4")] [assembly: AssemblyFileVersion("0.8.1.5")]
@@ -394,7 +394,7 @@ namespace Barotrauma
NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.White); NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.White);
})); }));
commands.Add(new Command("followsub", "followsub: Toggle whether the ", (string[] args) => commands.Add(new Command("followsub", "followsub: Toggle whether the camera should follow the nearest submarine.", (string[] args) =>
{ {
Camera.FollowSub = !Camera.FollowSub; Camera.FollowSub = !Camera.FollowSub;
NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.White); NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.White);
@@ -822,7 +822,7 @@ namespace Barotrauma.Networking
} }
else else
{ {
submarines.Add(new Submarine(Path.Combine(Submarine.SavePath, subName), subHash, false)); submarines.Add(new Submarine(Path.Combine(Submarine.SavePath, subName) + ".sub", subHash, false));
} }
} }
@@ -1441,6 +1441,8 @@ namespace Barotrauma.Networking
msg.Write((byte)ClientPermissions.Ban); msg.Write((byte)ClientPermissions.Ban);
msg.Write(kickedName); msg.Write(kickedName);
msg.Write(reason); msg.Write(reason);
msg.Write(range);
msg.Write(duration.HasValue ? duration.Value.TotalSeconds : 0.0); //0 = permaban
client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered);
} }
@@ -3,10 +3,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using GameAnalyticsSDK.Net;
#if WINDOWS #if WINDOWS
using System.Windows.Forms; using System.Windows.Forms;
using GameAnalyticsSDK.Net;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
#endif #endif
@@ -561,14 +561,18 @@ namespace Barotrauma
var playYourself = new GUITickBox(new Rectangle(0, 0, 20, 20), TextManager.Get("PlayYourself"), Alignment.TopLeft, myPlayerFrame); var playYourself = new GUITickBox(new Rectangle(0, 0, 20, 20), TextManager.Get("PlayYourself"), Alignment.TopLeft, myPlayerFrame);
playYourself.Selected = GameMain.NetworkMember.CharacterInfo != null; playYourself.Selected = GameMain.NetworkMember.CharacterInfo != null;
playYourself.OnSelected = TogglePlayYourself; playYourself.OnSelected = TogglePlayYourself;
playYourself.UserData = "playyourself"; playYourself.UserData = "playyourself";
GUIButton toggleHead = new GUIButton(new Rectangle(0, 50, 15, 15), "<", "", myPlayerFrame); GUIButton toggleHead = new GUIButton(new Rectangle(0, 50, 15, 15), "<", "", myPlayerFrame)
toggleHead.UserData = -1; {
toggleHead.OnClicked = ToggleHead; UserData = -1,
toggleHead = new GUIButton(new Rectangle(60, 50, 15, 15), ">", "", myPlayerFrame); OnClicked = ToggleHead
toggleHead.UserData = 1; };
toggleHead.OnClicked = ToggleHead; toggleHead = new GUIButton(new Rectangle(60, 50, 15, 15), ">", "", myPlayerFrame)
{
UserData = 1,
OnClicked = ToggleHead
};
new GUITextBlock(new Rectangle(100, 30, 200, 30), TextManager.Get("Gender"), "", myPlayerFrame); new GUITextBlock(new Rectangle(100, 30, 200, 30), TextManager.Get("Gender"), "", myPlayerFrame);
@@ -631,7 +635,10 @@ namespace Barotrauma
{ {
if (tickBox.Selected) if (tickBox.Selected)
{ {
GameMain.NetworkMember.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, GameMain.NetworkMember.Name, Gender.None, null); GameMain.NetworkMember.CharacterInfo =
new CharacterInfo(Character.HumanConfigFile, GameMain.NetworkMember.Name, GameMain.Config.CharacterGender, null);
GameMain.NetworkMember.CharacterInfo.HeadSpriteId = GameMain.Config.CharacterHeadIndex;
UpdatePlayerFrame(GameMain.NetworkMember.CharacterInfo); UpdatePlayerFrame(GameMain.NetworkMember.CharacterInfo);
} }
else else
@@ -1249,14 +1256,12 @@ namespace Barotrauma
private bool ToggleHead(GUIButton button, object userData) private bool ToggleHead(GUIButton button, object userData)
{ {
int dir = (int)userData;
if (GameMain.NetworkMember.CharacterInfo == null) return true; if (GameMain.NetworkMember.CharacterInfo == null) return true;
int dir = (int)userData;
GameMain.NetworkMember.CharacterInfo.HeadSpriteId += dir; GameMain.NetworkMember.CharacterInfo.HeadSpriteId += dir;
GameMain.Config.CharacterHeadIndex = GameMain.NetworkMember.CharacterInfo.HeadSpriteId;
UpdatePlayerHead(GameMain.NetworkMember.CharacterInfo); UpdatePlayerHead(GameMain.NetworkMember.CharacterInfo);
return true; return true;
} }
@@ -1264,7 +1269,7 @@ namespace Barotrauma
{ {
Gender gender = (Gender)obj; Gender gender = (Gender)obj;
GameMain.NetworkMember.CharacterInfo.Gender = gender; GameMain.NetworkMember.CharacterInfo.Gender = gender;
GameMain.Config.CharacterGender = GameMain.NetworkMember.CharacterInfo.Gender;
UpdatePlayerHead(GameMain.NetworkMember.CharacterInfo); UpdatePlayerHead(GameMain.NetworkMember.CharacterInfo);
return true; return true;
} }
@@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.8.1.4")] [assembly: AssemblyVersion("0.8.1.5")]
[assembly: AssemblyFileVersion("0.8.1.4")] [assembly: AssemblyFileVersion("0.8.1.5")]
@@ -14,7 +14,7 @@ namespace Barotrauma
{ {
lock (QueuedCommands) lock (QueuedCommands)
{ {
while (QueuedCommands.Count>0) while (QueuedCommands.Count > 0)
{ {
ExecuteCommand(QueuedCommands[0]); ExecuteCommand(QueuedCommands[0]);
QueuedCommands.RemoveAt(0); QueuedCommands.RemoveAt(0);
@@ -190,6 +190,16 @@ namespace Barotrauma
NewMessage(ent.ToString(), Color.Lime); NewMessage(ent.ToString(), Color.Lime);
} }
})); }));
//"dummy commands" that only exist so that the server can give clients permissions to use them
commands.Add(new Command("control|controlcharacter", "control [character name]: Start controlling the specified character (client-only).", null));
commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null));
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null));
commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null));
commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null));
commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null));
commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null));
#if DEBUG #if DEBUG
commands.Add(new Command("eventdata", "", (string[] args) => commands.Add(new Command("eventdata", "", (string[] args) =>
{ {
@@ -17,9 +17,11 @@
<Pickable holdpos="30,-15" handle1="0,1" slots="RightHand,LeftHand,Any"> <Pickable holdpos="30,-15" handle1="0,1" slots="RightHand,LeftHand,Any">
<StatusEffect type="OnFire" target="This" Condition="-100.0" disabledeltatime="true"> <StatusEffect type="OnFire" target="This" Condition="-100.0" disabledeltatime="true">
<Conditional Condition="gt 25"/>
<sound file="Content/Items/Reactor/explosion.ogg"/> <sound file="Content/Items/Reactor/explosion.ogg"/>
<Explosion range="250.0" structuredamage="10" damage="20" stun="5" force="3.0"/> <Explosion range="250.0" structuredamage="10" damage="20" stun="5" force="3.0"/>
</StatusEffect> </StatusEffect>
<StatusEffect type="OnFire" target="This" Condition="-100.0" disabledeltatime="true"/>
</Pickable> </Pickable>
</Item> </Item>
@@ -13,6 +13,7 @@
</fixrequirement> </fixrequirement>
<OxygenGenerator powerconsumption="1000.0" minvoltage="0.5" canbeselected = "true"> <OxygenGenerator powerconsumption="1000.0" minvoltage="0.5" canbeselected = "true">
<StatusEffect type="OnFire" target="This" Condition="-0.5"/> <StatusEffect type="OnFire" target="This" Condition="-0.5"/>
<StatusEffect type="OnActive" target="Contained" targetnames="Oxygen Tank" Condition="2.0"/> <StatusEffect type="OnActive" target="Contained" targetnames="Oxygen Tank" Condition="2.0"/>
<StatusEffect type="OnActive" target="Contained" targetnames="Oxygenite Tank" Condition="0.0"/> <StatusEffect type="OnActive" target="Contained" targetnames="Oxygenite Tank" Condition="0.0"/>
@@ -52,7 +52,7 @@ namespace Barotrauma
public float Range { get; private set; } public float Range { get; private set; }
[Serialize(0.0f, false)] [Serialize(0.0f, false)]
public float DamageRange { get; private set; } public float DamageRange { get; set; }
[Serialize(0.0f, false)] [Serialize(0.0f, false)]
public float Duration { get; private set; } public float Duration { get; private set; }
+164 -129
View File
@@ -88,6 +88,7 @@ namespace Barotrauma
public void Execute(string[] args) public void Execute(string[] args)
{ {
if (onExecute == null) return;
onExecute(args); onExecute(args);
} }
@@ -100,6 +101,7 @@ namespace Barotrauma
{ {
if (onClientRequestExecute == null) if (onClientRequestExecute == null)
{ {
if (onExecute == null) return;
onExecute(args); onExecute(args);
} }
else 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[] args) =>
{ {
string errorMsg; SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg);
SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out errorMsg);
if (!string.IsNullOrWhiteSpace(errorMsg)) if (!string.IsNullOrWhiteSpace(errorMsg))
{ {
ThrowError(errorMsg); ThrowError(errorMsg);
@@ -260,20 +261,18 @@ namespace Barotrauma
null, null,
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
string errorMsg; SpawnItem(args, cursorWorldPos, client.Character, out string errorMsg);
SpawnItem(args, cursorWorldPos, out errorMsg);
if (!string.IsNullOrWhiteSpace(errorMsg)) if (!string.IsNullOrWhiteSpace(errorMsg))
{ {
ThrowError(errorMsg); GameMain.Server.SendConsoleMessage(errorMsg, client);
} }
}, },
() => () =>
{ {
List<string> itemNames = new List<string>(); List<string> itemNames = new List<string>();
foreach (MapEntityPrefab prefab in MapEntityPrefab.List) foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{ {
ItemPrefab itemPrefab = prefab as ItemPrefab; if (prefab is ItemPrefab itemPrefab) itemNames.Add(itemPrefab.Name);
if (itemPrefab != null) itemNames.Add(itemPrefab.Name);
} }
return new string[][] 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) => commands.Add(new Command("disablecrewai", "disablecrewai: Disable the AI of the NPCs in the crew.", (string[] args) =>
{ {
HumanAIController.DisableCrewAI = true; HumanAIController.DisableCrewAI = true;
@@ -849,8 +849,7 @@ namespace Barotrauma
{ {
if (args.Length < 2) return; if (args.Length < 2) return;
int id; int.TryParse(args[0], out int id);
int.TryParse(args[0], out id);
var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id);
if (client == null) if (client == null)
{ {
@@ -891,8 +890,7 @@ namespace Barotrauma
return; return;
} }
int id; int.TryParse(args[0], out int id);
int.TryParse(args[0], out id);
var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id); var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id);
if (client == null) 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) => 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 int id);
int.TryParse(args[0], out id); var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id);
var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id);
if (client == null) if (client == null)
{ {
ThrowError("Client id \"" + id + "\" not found."); ThrowError("Client id \"" + id + "\" not found.");
@@ -1032,7 +1029,7 @@ namespace Barotrauma
ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"?", (reason) => 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) => 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 int id);
int.TryParse(args[0], out id); var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == id);
var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id);
if (client == null) if (client == null)
{ {
ThrowError("Client id \"" + id + "\" not found."); ThrowError("Client id \"" + id + "\" not found.");
return; return;
} }
ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"?", (reason) => 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) => 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; TimeSpan? banDuration = null;
if (!string.IsNullOrWhiteSpace(duration)) if (!string.IsNullOrWhiteSpace(duration))
{ {
TimeSpan parsedBanDuration; if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
if (!TryParseTimeSpan(duration, out parsedBanDuration))
{ {
ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\"."); ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
return; return;
@@ -1100,7 +1095,7 @@ namespace Barotrauma
banDuration = parsedBanDuration; 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; TimeSpan? banDuration = null;
if (!string.IsNullOrWhiteSpace(duration)) if (!string.IsNullOrWhiteSpace(duration))
{ {
TimeSpan parsedBanDuration; if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
if (!TryParseTimeSpan(duration, out parsedBanDuration))
{ {
ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\"."); ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
return; 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) => 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; Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
if (args.Length == 0)
{
tpCharacter = Character.Controlled;
}
else
{
tpCharacter = FindMatchingCharacter(args, false);
}
if (tpCharacter == null) return; if (tpCharacter == null) return;
var cam = GameMain.GameScreen.Cam; var cam = GameMain.GameScreen.Cam;
@@ -1167,17 +1210,7 @@ namespace Barotrauma
null, null,
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
Character tpCharacter = null; Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false);
if (args.Length == 0)
{
tpCharacter = client.Character;
}
else
{
tpCharacter = FindMatchingCharacter(args, false);
}
if (tpCharacter == null) return; if (tpCharacter == null) return;
var cam = GameMain.GameScreen.Cam; var cam = GameMain.GameScreen.Cam;
@@ -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) => 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; Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (args.Length == 0)
{
healedCharacter = Character.Controlled;
}
else
{
healedCharacter = FindMatchingCharacter(args);
}
if (healedCharacter != null) if (healedCharacter != null)
{ {
healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, null); healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, null);
@@ -1257,16 +1301,7 @@ namespace Barotrauma
null, null,
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
Character healedCharacter = null; Character healedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args);
if (args.Length == 0)
{
healedCharacter = client.Character;
}
else
{
healedCharacter = FindMatchingCharacter(args);
}
if (healedCharacter != null) if (healedCharacter != null)
{ {
healedCharacter.AddDamage(CauseOfDeath.Damage, -healedCharacter.MaxHealth, 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) => 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; Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (args.Length == 0)
{
revivedCharacter = Character.Controlled;
}
else
{
revivedCharacter = FindMatchingCharacter(args);
}
if (revivedCharacter == null) return; if (revivedCharacter == null) return;
revivedCharacter.Revive(false); revivedCharacter.Revive(false);
@@ -1313,16 +1339,7 @@ namespace Barotrauma
null, null,
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
Character revivedCharacter = null; Character revivedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args);
if (args.Length == 0)
{
revivedCharacter = client.Character;
}
else
{
revivedCharacter = FindMatchingCharacter(args);
}
if (revivedCharacter == null) return; if (revivedCharacter == null) return;
revivedCharacter.Revive(false); 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) => 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; Character ragdolledCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (args.Length == 0) if (ragdolledCharacter != null)
{ {
ragdolledCharacter = Character.Controlled; ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled;
} }
else },
{ null,
ragdolledCharacter = FindMatchingCharacter(args); (Client client, Vector2 cursorWorldPos, string[] args) =>
} {
Character ragdolledCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args);
if (ragdolledCharacter != null) if (ragdolledCharacter != null)
{ {
ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled; ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled;
@@ -1477,20 +1494,14 @@ namespace Barotrauma
commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) =>
{ {
Character killedCharacter = null; Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (args.Length == 0) killedCharacter?.AddDamage(CauseOfDeath.Damage, killedCharacter.MaxHealth * 2, null);
{ },
killedCharacter = Character.Controlled; null,
} (Client client, Vector2 cursorWorldPos, string[] args) =>
else {
{ Character killedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args);
killedCharacter = FindMatchingCharacter(args); killedCharacter?.AddDamage(CauseOfDeath.Damage, killedCharacter.MaxHealth * 2, null);
}
if (killedCharacter != null)
{
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) => 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 = ""; errorMsg = "";
if (args.Length < 1) return; if (args.Length < 1) return;
@@ -2128,23 +2139,48 @@ namespace Barotrauma
Vector2? spawnPos = null; Vector2? spawnPos = null;
Inventory spawnInventory = null; Inventory spawnInventory = null;
int extraParams = 0; if (args.Length > 1)
switch (args.Last())
{ {
case "cursor": switch (args[1])
extraParams = 1; {
spawnPos = cursorPos; case "cursor":
break; spawnPos = cursorPos;
case "inventory": break;
extraParams = 1; case "inventory":
spawnInventory = Character.Controlled == null ? null : Character.Controlled.Inventory; spawnInventory = controlledCharacter?.Inventory;
break; break;
default: default:
extraParams = 0; //Check if last arg matches the name of an in-game player
break; 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; var itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null) if (itemPrefab == null)
@@ -2162,7 +2198,6 @@ namespace Barotrauma
if (spawnPos != null) if (spawnPos != null)
{ {
Entity.Spawner.AddToSpawnQueue(itemPrefab, (Vector2)spawnPos); Entity.Spawner.AddToSpawnQueue(itemPrefab, (Vector2)spawnPos);
} }
else if (spawnInventory != null) else if (spawnInventory != null)
{ {
@@ -55,13 +55,7 @@ namespace Barotrauma
public override void Start() public override void Start()
{ {
base.Start(); base.Start();
if (GameMain.Server != null)
{
CargoManager.CreateItems();
}
lastUpdateID++; lastUpdateID++;
} }
@@ -108,10 +108,9 @@ namespace Barotrauma
codeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); codeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt);
codeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); codeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt);
while (traitorCount-- >= 0) while (traitorCount-- > 0)
{ {
if (traitorCandidates.Count <= 0) if (traitorCandidates.Count <= 0) break;
break;
int traitorIndex = Rand.Int(traitorCandidates.Count); int traitorIndex = Rand.Int(traitorCandidates.Count);
Character traitorCharacter = traitorCandidates[traitorIndex]; Character traitorCharacter = traitorCandidates[traitorIndex];
@@ -226,7 +226,14 @@ namespace Barotrauma
EventManager.StartRound(level); 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) if (GameSettings.SendUserStatistics)
{ {
@@ -234,7 +241,7 @@ namespace Barotrauma
GameAnalyticsSDK.Net.GameAnalytics.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start, GameAnalyticsSDK.Net.GameAnalytics.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
GameMode.Name, (Mission == null ? "None" : Mission.GetType().ToString())); GameMode.Name, (Mission == null ? "None" : Mission.GetType().ToString()));
} }
#if CLIENT #if CLIENT
roundSummary = new RoundSummary(this); roundSummary = new RoundSummary(this);
@@ -25,7 +25,7 @@ namespace Barotrauma
public bool EnableSplashScreen { get; set; } public bool EnableSplashScreen { get; set; }
//public bool FullScreenEnabled { get; set; } //public bool FullScreenEnabled { get; set; }
private KeyOrMouse[] keyMapping; private KeyOrMouse[] keyMapping;
private WindowMode windowMode; 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 public bool UnsavedSettings
{ {
get get
@@ -230,18 +259,15 @@ namespace Barotrauma
case "keymapping": case "keymapping":
foreach (XAttribute attribute in subElement.Attributes()) foreach (XAttribute attribute in subElement.Attributes())
{ {
InputType inputType; if (Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType))
if (Enum.TryParse(attribute.Name.ToString(), true, out inputType))
{ {
int mouseButton; if (int.TryParse(attribute.Value.ToString(), out int mouseButton))
if (int.TryParse(attribute.Value.ToString(), out mouseButton))
{ {
keyMapping[(int)inputType] = new KeyOrMouse(mouseButton); keyMapping[(int)inputType] = new KeyOrMouse(mouseButton);
} }
else else
{ {
Keys key; if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key))
if (Enum.TryParse(attribute.Value.ToString(), true, out key))
{ {
keyMapping[(int)inputType] = new KeyOrMouse(key); keyMapping[(int)inputType] = new KeyOrMouse(key);
} }
@@ -258,6 +284,9 @@ namespace Barotrauma
break; break;
case "player": case "player":
defaultPlayerName = subElement.GetAttributeString("name", ""); 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; break;
} }
} }
@@ -363,8 +392,10 @@ namespace Barotrauma
gameplay.Add(jobPreferences); gameplay.Add(jobPreferences);
doc.Root.Add(gameplay); doc.Root.Add(gameplay);
var playerElement = new XElement("player"); var playerElement = new XElement("player",
playerElement.Add(new XAttribute("name", defaultPlayerName ?? "")); new XAttribute("name", defaultPlayerName ?? ""),
new XAttribute("headindex", characterHeadIndex),
new XAttribute("gender", characterGender));
doc.Root.Add(playerElement); doc.Root.Add(playerElement);
doc.Save(filePath); doc.Save(filePath);
@@ -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) public override bool Use(float deltaTime, Character character = null)
{ {
if (character != null && !characterUsable) return false; if (character != null && !characterUsable) return false;
@@ -196,7 +216,6 @@ namespace Barotrauma.Items.Components
if (OnProjectileCollision(fixture, normal)) if (OnProjectileCollision(fixture, normal))
{ {
hitSomething = true; hitSomething = true;
//Character.Controlled.AnimController.Teleport(point - Character.Controlled.SimPosition, Vector2.Zero);
break; break;
} }
} }
@@ -281,8 +300,7 @@ namespace Barotrauma.Items.Components
Character character = null; Character character = null;
if (attack != null) if (attack != null)
{ {
var submarine = target.Body.UserData as Submarine; if (target.Body.UserData is Submarine submarine)
if (submarine != null)
{ {
item.Move(-submarine.Position); item.Move(-submarine.Position);
item.Submarine = submarine; item.Submarine = submarine;
@@ -290,9 +308,8 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
Limb limb = target.Body.UserData as Limb;
Structure structure; Structure structure;
if (limb != null) if (target.Body.UserData is Limb limb)
{ {
attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f);
if (limb.character != null) if (limb.character != null)
@@ -350,13 +367,12 @@ namespace Barotrauma.Items.Components
{ {
contained.SetTransform(item.SimPosition, contained.body.Rotation); contained.SetTransform(item.SimPosition, contained.body.Rotation);
} }
//contained.Condition = 0.0f; //Let the freaking .xml handle it jeez
} }
} }
if (RemoveOnHit) if (RemoveOnHit)
{ {
Item.Spawner.AddToRemoveQueue(item); Entity.Spawner.AddToRemoveQueue(item);
} }
return true; return true;
@@ -315,9 +315,9 @@ namespace Barotrauma
public override string ToString() public override string ToString()
{ {
#if CLIENT #if CLIENT
return (GameMain.DebugDraw) ? Name + "(ID: " + ID + ")" : Name; return (GameMain.DebugDraw) ? Name + " (ID: " + ID + ")" : Name;
#elif SERVER #elif SERVER
return Name + "(ID: " + ID + ")"; return Name + " (ID: " + ID + ")";
#endif #endif
} }
@@ -80,7 +80,7 @@ namespace Barotrauma
if (distSqr > displayRangeSqr) continue; if (distSqr > displayRangeSqr) continue;
//ignore reactors (don't want to blow them up) //ignore reactors (don't want to blow them up)
if (item.GetComponent<Reactor>() == null) continue; if (item.GetComponent<Reactor>() != null) continue;
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
@@ -664,19 +664,20 @@ namespace Barotrauma
float damageAmount = 0.0f; float damageAmount = 0.0f;
for (int i = 0; i < SectionCount; i++) 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); damageAmount = attack.GetStructureDamage(deltaTime);
AddDamage(i, damageAmount, attacker); AddDamage(i, damageAmount, attacker);
#if CLIENT #if CLIENT
GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f);
#endif #endif
} }
} }
#if CLIENT #if CLIENT
if (playSound)// && !SectionBodyDisabled(i)) if (playSound)
{ {
string damageSoundType = (attack.DamageType == DamageType.Blunt) ? "StructureBlunt" : "StructureSlash"; string damageSoundType = (attack.DamageType == DamageType.Blunt) ? "StructureBlunt" : "StructureSlash";
SoundPlayer.PlayDamageSound(damageSoundType, damageAmount, worldPosition, tags: Tags); SoundPlayer.PlayDamageSound(damageSoundType, damageAmount, worldPosition, tags: Tags);
@@ -29,10 +29,20 @@ namespace Barotrauma.Networking
public byte TeamID = 0; 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 CharacterInfo CharacterInfo;
public NetConnection Connection { get; set; } 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; public UInt16 LastRecvGeneralUpdate = 0;
@@ -780,11 +780,21 @@ namespace Barotrauma.Networking
case ClientPermissions.Ban: case ClientPermissions.Ban:
string bannedName = inc.ReadString().ToLowerInvariant(); string bannedName = inc.ReadString().ToLowerInvariant();
string banReason = inc.ReadString(); string banReason = inc.ReadString();
bool range = inc.ReadBoolean();
double durationSeconds = inc.ReadDouble();
var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == bannedName); var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == bannedName);
if (bannedClient != null) if (bannedClient != null)
{ {
Log("Client \"" + sender.Name + "\" banned \"" + bannedClient.Name + "\".", ServerLog.MessageType.ServerMessage); 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; break;
case ClientPermissions.EndRound: case ClientPermissions.EndRound:
@@ -1343,11 +1353,12 @@ namespace Barotrauma.Networking
List<Character> characters = new List<Character>(); List<Character> characters = new List<Character>();
foreach (Client client in ConnectedClients) foreach (Client client in ConnectedClients)
{ {
if (client.Character != null) if (client.Character != null) characters.Add(client.Character);
characters.Add(client.Character);
} }
var max = (int)Math.Round(characters.Count * 0.2f, 1); if (Character != null) characters.Add(Character);
var traitorCount = Math.Max(1, TraitorsEnabled == YesNoMaybe.Maybe ? Rand.Int(max) + 1 : max);
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); TraitorManager = new TraitorManager(this, traitorCount);
if (TraitorManager.TraitorList.Count > 0) if (TraitorManager.TraitorList.Count > 0)
@@ -1501,6 +1512,7 @@ namespace Barotrauma.Networking
foreach (Client client in connectedClients) foreach (Client client in connectedClients)
{ {
client.Character = null; client.Character = null;
client.HasSpawned = false;
client.InGame = false; client.InGame = false;
} }
} }
@@ -1620,6 +1632,7 @@ namespace Barotrauma.Networking
} }
client.Character = null; client.Character = null;
client.HasSpawned = false;
client.InGame = false; client.InGame = false;
if (string.IsNullOrWhiteSpace(msg)) msg = client.Name + " has left the server"; if (string.IsNullOrWhiteSpace(msg)) msg = client.Name + " has left the server";
@@ -111,11 +111,11 @@ namespace Barotrauma
#endif #endif
break; break;
case VoteType.EndRound: case VoteType.EndRound:
if (sender.Character == null) return; if (!sender.HasSpawned) return;
sender.SetVote(voteType, inc.ReadBoolean()); sender.SetVote(voteType, inc.ReadBoolean());
GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.Character != null && c.GetVote<bool>(VoteType.EndRound)); GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote<bool>(VoteType.EndRound));
GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.Character != null); GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned);
break; break;
case VoteType.Kick: case VoteType.Kick:
+31
View File
@@ -1,3 +1,34 @@
---------------------------------------------------------------------------------------------------------
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 v0.8.1.4
--------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------