(bf212a41f) v0.9.2.0 pre-release test version

This commit is contained in:
Joonas Rikkonen
2019-07-27 21:06:07 +03:00
parent afa2137bd2
commit 0f63da27b2
154 changed files with 3959 additions and 1428 deletions
+5 -1
View File
@@ -120,12 +120,16 @@ namespace Barotrauma
get { return targetPos; }
set { targetPos = value; }
}
public Vector2 GetPosition()
{
return position;
}
// Auxiliary function to move the camera
public void Translate(Vector2 amount)
{
position += amount;
}
public void UpdateTransform(bool interpolate = true, bool clampPos = false)
@@ -11,22 +11,9 @@ namespace Barotrauma
{
}
partial void AdjustKarma(Character attacker, AttackResult attackResult)
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult)
{
if (attacker == null) return;
Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker);
if (attackerClient == null) return;
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (targetClient != null)
{
if (attacker.TeamID == TeamID)
{
attackerClient.Karma -= attackResult.Damage * 0.01f;
if (CharacterHealth.MaxVitality <= CharacterHealth.MinVitality) attackerClient.Karma = 0.0f;
}
}
GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, attackResult.Afflictions);
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction)
@@ -21,29 +21,25 @@ namespace Barotrauma
{
if (!Enabled) { return 1000.0f; }
if (recipient.Character == null || recipient.Character.IsDead)
{
return 0.2f;
}
else
{
float distance = Vector2.Distance(recipient.Character.WorldPosition, WorldPosition);
float priority = 1.0f - MathUtils.InverseLerp(
NetConfig.HighPrioCharacterPositionUpdateDistance,
NetConfig.LowPrioCharacterPositionUpdateDistance,
distance);
Vector2 comparePosition = recipient.SpectatePos == null ? recipient.Character.WorldPosition : recipient.SpectatePos.Value;
float interval = MathHelper.Lerp(
NetConfig.LowPrioCharacterPositionUpdateInterval,
NetConfig.HighPrioCharacterPositionUpdateInterval,
priority);
float distance = Vector2.Distance(comparePosition, WorldPosition);
float priority = 1.0f - MathUtils.InverseLerp(
NetConfig.HighPrioCharacterPositionUpdateDistance,
NetConfig.LowPrioCharacterPositionUpdateDistance,
distance);
if (IsDead)
{
interval = Math.Max(interval * 2, 0.1f);
}
return interval;
float interval = MathHelper.Lerp(
NetConfig.LowPrioCharacterPositionUpdateInterval,
NetConfig.HighPrioCharacterPositionUpdateInterval,
priority);
if (IsDead)
{
interval = Math.Max(interval * 2, 0.1f);
}
return interval;
}
partial void UpdateNetInput()
@@ -86,7 +86,8 @@ namespace Barotrauma
int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)Console.WindowWidth), 1);
Console.CursorLeft = 0;
Console.Write(new string(' ', consoleWidth));
Console.CursorTop -= inputLines; Console.CursorLeft = 0;
Console.CursorTop = Math.Max(Console.CursorTop - inputLines, 0);
Console.CursorLeft = 0;
while (queuedMessages.Count > 0)
{
ColoredText msg = queuedMessages.Dequeue();
@@ -121,9 +122,9 @@ namespace Barotrauma
switch (key.Key)
{
case ConsoleKey.Enter:
lock (DebugConsole.QueuedCommands)
lock (QueuedCommands)
{
DebugConsole.QueuedCommands.Add(input);
QueuedCommands.Add(input);
}
input = "";
memoryIndex = -1;
@@ -219,7 +220,15 @@ namespace Barotrauma
private static void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute)
{
commands.First(c => c.names.Intersect(names.Split('|')).Count() > 0).OnClientRequestExecute = onClientRequestExecute;
var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0);
if (matchingCommand == null)
{
throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found.");
}
else
{
matchingCommand.OnClientRequestExecute = onClientRequestExecute;
}
}
private static void InitProjectSpecific()
@@ -568,10 +577,8 @@ namespace Barotrauma
NewMessage(client.Name + " has the following permissions:", Color.White);
foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
{
if (permission == ClientPermissions.None || !client.HasPermission(permission)) continue;
System.Reflection.FieldInfo fi = typeof(ClientPermissions).GetField(permission.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
NewMessage(" - " + attributes[0].Description, Color.White);
if (permission == ClientPermissions.None || !client.HasPermission(permission)) { continue; }
NewMessage(" - " + TextManager.Get("ClientPermission." + permission), Color.White);
}
if (client.HasPermission(ClientPermissions.ConsoleCommands))
{
@@ -590,12 +597,100 @@ namespace Barotrauma
}
});
/*AssignOnExecute("togglekarma", (string[] args) =>
AssignOnExecute("togglekarma", (string[] args) =>
{
return;
if (GameMain.Server == null) return;
GameMain.Server.ServerSettings.KarmaEnabled = !GameMain.Server.ServerSettings.KarmaEnabled;
});*/
NewMessage(GameMain.Server.ServerSettings.KarmaEnabled ? "Karma system enabled." : "Karma system disabled.", Color.LightGreen);
});
AssignOnExecute("resetkarma", (string[] args) =>
{
if (GameMain.Server == null || args.Length == 0) return;
var client = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]);
if (client == null)
{
ThrowError("Client \"" + args[0] + "\" not found.");
return;
}
client.Karma = 100.0f;
NewMessage("Set the karma of the client \"" + args[0] + "\" to 100.", Color.LightGreen);
});
AssignOnClientRequestExecute("resetkarma", (Client client, Vector2 cursorWorldPos, string[] args) =>
{
if (GameMain.Server == null || args.Length == 0) return;
var targetClient = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]);
if (targetClient == null)
{
ThrowError("Client \"" + args[0] + "\" not found.");
return;
}
targetClient.Karma = 100.0f;
GameMain.Server.SendDirectChatMessage("Set the karma of the client \"" + args[0] + "\" to 100.", client);
NewMessage("Client \"" + client.Name + "\" set the karma of \"" + args[0] + "\" to 100.", Color.LightGreen);
});
AssignOnExecute("setkarma", (string[] args) =>
{
if (GameMain.Server == null || args.Length < 2) return;
var client = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]);
if (client == null)
{
ThrowError("Client \"" + args[0] + "\" not found.");
return;
}
if (!float.TryParse(args[1], out float karmaValue) || karmaValue < 0.0f || karmaValue > 100.0f)
{
ThrowError("\"" + args[1] + "\" is not a valid karma value. You need to enter a number between 0-100.");
return;
}
client.Karma = karmaValue;
NewMessage("Set the karma of the client \"" + args[0] + "\" to " + karmaValue + ".", Color.LightGreen);
});
AssignOnClientRequestExecute("setkarma", (Client client, Vector2 cursorWorldPos, string[] args) =>
{
if (GameMain.Server == null || args.Length < 2) return;
var targetClient = GameMain.Server.ConnectedClients.Find(c => c.Name == args[0]);
if (targetClient == null)
{
GameMain.Server.SendDirectChatMessage("Client \"" + args[0] + "\" not found.", client);
return;
}
if (!float.TryParse(args[1], out float karmaValue) || karmaValue < 0.0f || karmaValue > 100.0f)
{
GameMain.Server.SendDirectChatMessage("\"" + args[1] + "\" is not a valid karma value. You need to enter a number between 0-100.", client);
return;
}
targetClient.Karma = karmaValue;
GameMain.Server.SendDirectChatMessage("Set the karma of the client \"" + args[0] + "\" to " + karmaValue + ".", client);
NewMessage("Client \"" + client.Name + "\" set the karma of \"" + args[0] + "\" to " + karmaValue + ".", Color.LightGreen);
});
AssignOnExecute("showkarma", (string[] args) =>
{
if (GameMain.Server == null) return;
NewMessage("***************", Color.Cyan);
foreach (Client c in GameMain.Server.ConnectedClients)
{
NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Karma, Color.Cyan);
}
NewMessage("***************", Color.Cyan);
});
AssignOnClientRequestExecute("showkarma", (Client client, Vector2 cursorWorldPos, string[] args) =>
{
GameMain.Server.SendConsoleMessage("***************", client);
foreach (Client c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Karma, client);
}
GameMain.Server.SendConsoleMessage("***************", client);
});
AssignOnExecute("togglekarmatestmode|karmatestmode", (string[] args) =>
{
if (GameMain.Server?.KarmaManager == null) return;
GameMain.Server.KarmaManager.TestMode = !GameMain.Server.KarmaManager.TestMode;
NewMessage(GameMain.Server.KarmaManager.TestMode ? "Karma test mode enabled." : "Karma test mode disabled.", Color.LightGreen);
});
AssignOnExecute("banip", (string[] args) =>
{
@@ -989,6 +1084,16 @@ namespace Barotrauma
};
}));
AssignOnExecute("respawnnow", (string[] args) =>
{
if (GameMain.Server?.RespawnManager == null) { return; }
if (GameMain.Server.RespawnManager.CurrentState != RespawnManager.State.Transporting)
{
GameMain.Server.RespawnManager.ForceRespawn();
}
});
commands.Add(new Command("startgame|startround|start", "start/startgame/startround: Start a new round.", (string[] args) =>
{
if (Screen.Selected == GameMain.GameScreen) return;
@@ -1523,7 +1628,11 @@ namespace Barotrauma
"showperm",
(Client senderClient, Vector2 cursorWorldPos, string[] args) =>
{
if (args.Length < 2) return;
if (args.Length < 1)
{
GameMain.Server.SendConsoleMessage("showperm [id]: Shows the current administrative permissions of the client with the specified client ID.", senderClient);
return;
}
int.TryParse(args[0], out int id);
var client = GameMain.Server.ConnectedClients.Find(c => c.ID == id);
@@ -1542,10 +1651,8 @@ namespace Barotrauma
GameMain.Server.SendConsoleMessage(client.Name + " has the following permissions:", senderClient);
foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
{
if (permission == ClientPermissions.None || !client.HasPermission(permission)) continue;
System.Reflection.FieldInfo fi = typeof(ClientPermissions).GetField(permission.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
GameMain.Server.SendConsoleMessage(" - " + attributes[0].Description, senderClient);
if (permission == ClientPermissions.None || !client.HasPermission(permission)) { continue; }
GameMain.Server.SendConsoleMessage(" - " + TextManager.Get("ClientPermission." + permission), senderClient);
}
if (client.HasPermission(ClientPermissions.ConsoleCommands))
{
@@ -8,6 +8,9 @@ namespace Barotrauma
{
private bool[] teamDead = new bool[2];
private bool initialized = false;
private int state = 0;
public override string Description
{
get
@@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components
{
partial class Door
{
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage)
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen)
{
if (isStuck || isOpen == open)
{
@@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components
if (sendNetworkMessage)
{
item.CreateServerEvent(this);
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), forcedOpen });
}
}
@@ -27,6 +27,7 @@ namespace Barotrauma.Items.Components
base.ServerWrite(msg, c, extraData);
msg.Write(isOpen);
msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open
msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8);
}
}
@@ -7,5 +7,11 @@ namespace Barotrauma.Items.Components
public bool MaintainPos;
public bool LevelStartSelected;
public bool LevelEndSelected;
public bool UnsentChanges
{
get { return unsentChanges; }
set { unsentChanges = value; }
}
}
}
@@ -5,12 +5,6 @@ namespace Barotrauma.Items.Components
{
partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable
{
void InitProjSpecific()
{
//let the clients know the initial deterioration delay
item.CreateServerEvent(this);
}
public void ServerRead(ClientNetObject type, NetBuffer msg, Client c)
{
if (c.Character == null) return;
@@ -33,6 +33,18 @@ namespace Barotrauma.Items.Components
}
}
List<Wire> clientSideDisconnectedWires = new List<Wire>();
ushort disconnectedWireCount = msg.ReadUInt16();
for (int i = 0; i < disconnectedWireCount; i++)
{
ushort wireId = msg.ReadUInt16();
if (!(Entity.FindEntityByID(wireId) is Item wireItem)) { continue; }
Wire wireComponent = wireItem.GetComponent<Wire>();
if (wireComponent == null) { continue; }
clientSideDisconnectedWires.Add(wireComponent);
}
//don't allow rewiring locked panels
if (Locked || !GameMain.NetworkMember.ServerSettings.AllowRewiring) { return; }
@@ -47,7 +59,7 @@ namespace Barotrauma.Items.Components
{
//wire not found in any of the connections yet (client is trying to connect a new wire)
// -> we need to check if the client has access to it
if (!Connections.Any(connection => connection.Wires.Contains(wire)))
if (!Connections.Any(connection => connection.Wires.Contains(wire)) && !DisconnectedWires.Contains(wire))
{
if (!wire.Item.CanClientAccess(c)) { return; }
}
@@ -75,11 +87,19 @@ namespace Barotrauma.Items.Components
}
existingWire.RemoveConnection(item);
item.GetComponent<ConnectionPanel>()?.DisconnectedWires.Add(existingWire);
GameMain.Server.KarmaManager.OnWireDisconnected(c.Character, existingWire);
if (existingWire.Connections[0] == null && existingWire.Connections[1] == null)
{
GameServer.Log(c.Character.LogName + " disconnected a wire from " +
Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.ItemInteraction);
if (!clientSideDisconnectedWires.Contains(existingWire))
{
existingWire.Item.Drop(c.Character);
}
}
else if (existingWire.Connections[0] != null)
{
@@ -89,24 +109,24 @@ namespace Barotrauma.Items.Components
//wires that are not in anyone's inventory (i.e. not currently being rewired)
//can never be connected to only one connection
// -> the client must have dropped the wire from the connection panel
if (existingWire.Item.ParentInventory == null && !wires.Any(w => w.Contains(existingWire)))
/*if (existingWire.Item.ParentInventory == null && !wires.Any(w => w.Contains(existingWire)))
{
//let other clients know the item was also disconnected from the other connection
existingWire.Connections[0].Item.CreateServerEvent(existingWire.Connections[0].Item.GetComponent<ConnectionPanel>());
existingWire.Item.Drop(c.Character);
}
}*/
}
else if (existingWire.Connections[1] != null)
{
GameServer.Log(c.Character.LogName + " disconnected a wire from " +
Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + existingWire.Connections[1].Item.Name + " (" + existingWire.Connections[1].Name + ")", ServerLog.MessageType.ItemInteraction);
if (existingWire.Item.ParentInventory == null && !wires.Any(w => w.Contains(existingWire)))
/*if (existingWire.Item.ParentInventory == null && !wires.Any(w => w.Contains(existingWire)))
{
//let other clients know the item was also disconnected from the other connection
existingWire.Connections[1].Item.CreateServerEvent(existingWire.Connections[1].Item.GetComponent<ConnectionPanel>());
existingWire.Item.Drop(c.Character);
}
}*/
}
Connections[i].SetWire(j, null);
@@ -114,6 +134,17 @@ namespace Barotrauma.Items.Components
}
}
foreach (Wire disconnectedWire in DisconnectedWires.ToList())
{
if (disconnectedWire.Connections[0] == null &&
disconnectedWire.Connections[1] == null &&
!clientSideDisconnectedWires.Contains(disconnectedWire))
{
disconnectedWire.Item.Drop(c.Character);
GameServer.Log(c.Character.LogName + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory);
}
}
//go through new wires
for (int i = 0; i < Connections.Count; i++)
{
@@ -186,6 +186,15 @@ namespace Barotrauma
case NetEntityEvent.Type.ChangeProperty:
ReadPropertyChange(msg, true, c);
break;
case NetEntityEvent.Type.Combine:
UInt16 combineTargetID = msg.ReadUInt16();
Item combineTarget = FindEntityByID(combineTargetID) as Item;
if (combineTarget == null || !c.Character.CanInteractWith(this) || !c.Character.CanInteractWith(combineTarget))
{
return;
}
Combine(combineTarget);
break;
}
}
@@ -5,24 +5,9 @@ namespace Barotrauma
{
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
{
partial void AdjustKarma(IDamageable attacker, float amount)
partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount)
{
if (GameMain.Server != null)
{
if (Submarine == null) return;
if (attacker == null) return;
if (attacker is Character attackerCharacter)
{
Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attackerCharacter);
if (attackerClient != null)
{
if (attackerCharacter.TeamID == Submarine.TeamID)
{
attackerClient.Karma -= amount * 0.001f;
}
}
}
}
GameMain.Server.KarmaManager.OnStructureHealthChanged(this, attacker, damageAmount);
}
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
@@ -83,8 +83,9 @@ namespace Barotrauma.Networking
if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner)
{
c.ChatSpamCount++;
GameMain.Server.KarmaManager.OnSpamFilterTriggered(c);
c.ChatSpamCount++;
if (c.ChatSpamCount > 3)
{
//kick for spamming too much
@@ -65,20 +65,19 @@ namespace Barotrauma.Networking
public bool SpectateOnly;
private float karma = 1.0f;
private float karma = 100.0f;
public float Karma
{
get
{
if (GameMain.Server == null) return 1.0f;
if (!GameMain.Server.ServerSettings.KarmaEnabled) return 1.0f;
if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return 100.0f; }
if (HasPermission(ClientPermissions.KarmaImmunity)) { return 100.0f; }
return karma;
}
set
{
if (GameMain.Server == null) return;
if (!GameMain.Server.ServerSettings.KarmaEnabled) return;
karma = Math.Min(Math.Max(value, 0.0f), 1.0f);
if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return; }
karma = Math.Min(Math.Max(value, 0.0f), 100.0f);
}
}
@@ -78,7 +78,7 @@ namespace Barotrauma.Networking
{
get { return entityEventManager; }
}
public TimeSpan UpdateInterval
{
get { return updateInterval; }
@@ -109,7 +109,6 @@ namespace Barotrauma.Networking
LastClientListUpdateID = 0;
NetPeerConfiguration = new NetPeerConfiguration("barotrauma");
NetPeerConfiguration.Port = port;
Port = port;
QueryPort = queryPort;
@@ -119,12 +118,12 @@ namespace Barotrauma.Networking
NetPeerConfiguration.EnableUPnP = true;
}
serverSettings = new ServerSettings(name, port, queryPort, maxPlayers, isPublic, attemptUPnP);
serverSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP);
if (!string.IsNullOrEmpty(password))
{
serverSettings.SetPassword(password);
}
NetPeerConfiguration.MaximumConnections = maxPlayers * 2; //double the lidgren connections for unauthenticated players
NetPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage |
@@ -353,6 +352,7 @@ namespace Barotrauma.Networking
unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f);
fileSender.Update(deltaTime);
KarmaManager.UpdateClients(ConnectedClients, deltaTime);
if (serverSettings.VoiceChatEnabled)
{
@@ -361,7 +361,7 @@ namespace Barotrauma.Networking
if (gameStarted)
{
if (respawnManager != null) respawnManager.Update(deltaTime);
if (respawnManager != null) { respawnManager.Update(deltaTime); }
entityEventManager.Update(connectedClients);
@@ -415,25 +415,32 @@ namespace Barotrauma.Networking
}
}
if (isCrewDead && respawnManager == null)
float endRoundDelay = 1.0f;
if (serverSettings.AutoRestart && isCrewDead)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
}
else if (serverSettings.EndRoundAtLevelEnd && subAtLevelEnd)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
}
else if (isCrewDead && respawnManager == null)
{
if (endRoundTimer <= 0.0f)
{
SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60"), ChatMessageType.Server);
}
endRoundDelay = 60.0f;
endRoundTimer += deltaTime;
}
else
{
endRoundTimer = 0.0f;
}
//restart if all characters are dead or submarine is at the end of the level
if ((serverSettings.AutoRestart && isCrewDead)
||
(serverSettings.EndRoundAtLevelEnd && subAtLevelEnd)
||
(isCrewDead && respawnManager == null && endRoundTimer >= 60.0f))
if (endRoundTimer >= endRoundDelay)
{
if (serverSettings.AutoRestart && isCrewDead)
{
@@ -1029,6 +1036,9 @@ namespace Barotrauma.Networking
case ClientNetObject.VOTE:
serverSettings.Voting.ServerRead(inc, c);
break;
case ClientNetObject.SPECTATING_POS:
c.SpectatePos = new Vector2(inc.ReadFloat(), inc.ReadFloat());
break;
default:
return;
}
@@ -1346,11 +1356,20 @@ namespace Barotrauma.Networking
foreach (Character character in Character.CharacterList)
{
if (!character.Enabled) continue;
if (c.Character != null &&
Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition) >=
NetConfig.DisableCharacterDistSqr)
if (c.SpectatePos == null)
{
continue;
if (c.Character != null && Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition) >= NetConfig.DisableCharacterDistSqr)
{
continue;
}
}
else
{
if (Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= NetConfig.DisableCharacterDistSqr)
{
continue;
}
}
float updateInterval = character.GetPositionUpdateInterval(c);
@@ -2005,8 +2024,16 @@ namespace Barotrauma.Networking
public void EndGame()
{
if (!gameStarted) return;
Log("Ending the round...", ServerLog.MessageType.ServerMessage);
if (!gameStarted) { return; }
if (GameSettings.VerboseLogging)
{
Log("Ending the round...\n" + Environment.StackTrace, ServerLog.MessageType.ServerMessage);
}
else
{
Log("Ending the round...", ServerLog.MessageType.ServerMessage);
}
string endMessage = "The round has ended." + '\n';
@@ -2068,31 +2095,14 @@ namespace Barotrauma.Networking
}
}
CoroutineManager.StartCoroutine(EndCinematic(), "EndCinematic");
GameMain.NetLobbyScreen.RandomizeSettings();
}
public IEnumerable<object> EndCinematic()
{
float endPreviewLength = 10.0f;
var cinematic = new RoundEndCinematic(Submarine.MainSub, GameMain.GameScreen.Cam, endPreviewLength);
do
{
yield return CoroutineStatus.Running;
} while (cinematic.Running);
Submarine.Unload();
entityEventManager.Clear();
GameMain.NetLobbyScreen.Select();
Log("Round ended.", ServerLog.MessageType.ServerMessage);
yield return CoroutineStatus.Success;
GameMain.NetLobbyScreen.RandomizeSettings();
}
public override void AddChatMessage(ChatMessage message)
{
if (string.IsNullOrEmpty(message.Text)) { return; }
@@ -2257,6 +2267,7 @@ namespace Barotrauma.Networking
previousPlayers.Add(previousPlayer);
}
previousPlayer.Name = client.Name;
previousPlayer.Karma = client.Karma;
previousPlayer.KickVoters.Clear();
foreach (Client c in connectedClients)
{
@@ -2267,6 +2278,8 @@ namespace Barotrauma.Networking
client.Dispose();
connectedClients.Remove(client);
KarmaManager.OnClientDisconnected(client);
UpdateVoteStatus();
SendChatMessage(msg, ChatMessageType.Server);
@@ -3077,6 +3090,7 @@ namespace Barotrauma.Networking
public string Name;
public string IP;
public UInt64 SteamID;
public float Karma;
public readonly List<Client> KickVoters = new List<Client>();
public PreviousPlayer(Client c)
@@ -472,9 +472,10 @@ namespace Barotrauma.Networking
unauthClient = null;
ConnectedClients.Add(newClient);
var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient));
var previousPlayer = previousPlayers.Find(p => p.MatchesClient(newClient));
if (previousPlayer != null)
{
newClient.Karma = previousPlayer.Karma;
foreach (Client c in previousPlayer.KickVoters)
{
if (!connectedClients.Contains(c)) { continue; }
@@ -0,0 +1,337 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class KarmaManager : ISerializableEntity
{
private class ClientMemory
{
public List<Pair<Wire, float>> WireDisconnectTime = new List<Pair<Wire, float>>();
//the client's karma value when they were last sent a notification about it (e.g. "your karma is very low")
public float PreviousNotifiedKarma;
public float StructureDamageAccumulator;
private float structureDamagePerSecond;
public float StructureDamagePerSecond
{
get { return Math.Max(StructureDamageAccumulator, structureDamagePerSecond); }
set { structureDamagePerSecond = value; }
}
}
public bool TestMode = false;
private readonly Dictionary<Client, ClientMemory> clientMemories = new Dictionary<Client, ClientMemory>();
private readonly List<Client> bannedClients = new List<Client>();
private DateTime perSecondUpdate;
private double KarmaNotificationTime;
public void UpdateClients(IEnumerable<Client> clients, float deltaTime)
{
if (!GameMain.Server.GameStarted) { return; }
bannedClients.Clear();
foreach (Client client in clients)
{
var clientMemory = GetClientMemory(client);
UpdateClient(client, deltaTime);
if (perSecondUpdate < DateTime.Now)
{
clientMemory.StructureDamagePerSecond = clientMemory.StructureDamageAccumulator;
clientMemory.StructureDamageAccumulator = 0.0f;
}
}
if (perSecondUpdate < DateTime.Now)
{
perSecondUpdate = DateTime.Now + new TimeSpan(0, 0, 1);
}
if (TestMode || Timing.TotalTime > KarmaNotificationTime)
{
foreach (Client client in clients)
{
SendKarmaNotifications(client);
}
KarmaNotificationTime = Timing.TotalTime + KarmaNotificationInterval;
}
foreach (Client bannedClient in bannedClients)
{
GameMain.Server.BanClient(bannedClient, $"KarmaBanned~[banthreshold]={(int)KickBanThreshold}", duration: TimeSpan.FromSeconds(GameMain.Server.ServerSettings.AutoBanTime));
}
}
private void SendKarmaNotifications(Client client, string debugKarmaChangeReason = "")
{
var clientMemory = GetClientMemory(client);
float karmaChange = client.Karma - clientMemory.PreviousNotifiedKarma;
if (Math.Abs(karmaChange) > KarmaNotificationInterval || (TestMode && Math.Abs(karmaChange) > 2.0f))
{
if (TestMode)
{
string msg =
karmaChange < 0 ? $"You karma has decreased to {client.Karma}" : $"You karma has increased to {client.Karma}";
if (!string.IsNullOrEmpty(debugKarmaChangeReason))
{
msg += $". Reason: {debugKarmaChangeReason}";
}
GameMain.Server.SendDirectChatMessage(msg, client);
}
else if (Math.Abs(KickBanThreshold - client.Karma) < KarmaNotificationInterval)
{
GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client);
}
else
{
GameMain.Server.SendDirectChatMessage(TextManager.Get(karmaChange < 0 ? "KarmaDecreasedUnknownAmount" : "KarmaIncreasedUnknownAmount"), client);
}
clientMemory.PreviousNotifiedKarma = client.Karma;
}
}
private void UpdateClient(Client client, float deltaTime)
{
if (client.Karma > KarmaDecayThreshold)
{
client.Karma -= KarmaDecay * deltaTime;
}
else if (client.Karma < KarmaIncreaseThreshold)
{
client.Karma += KarmaIncrease * deltaTime;
}
if (client.Character != null && !client.Character.Removed)
{
//increase the strength of the herpes affliction in steps instead of linearly
//otherwise clients could determine their exact karma value from the strength
float herpesStrength = 0.0f;
if (client.Karma < 20)
herpesStrength = 100.0f;
else if (client.Karma < 30)
herpesStrength = 60.0f;
else if (client.Karma < 40.0f)
herpesStrength = 30.0f;
var existingAffliction = client.Character.CharacterHealth.GetAffliction<AfflictionSpaceHerpes>("spaceherpes");
if (existingAffliction == null && herpesStrength > 0.0f)
{
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
}
else if (existingAffliction != null)
{
existingAffliction.Strength = herpesStrength;
}
//check if the client has disconnected an excessive number of wires
var clientMemory = GetClientMemory(client);
if (clientMemory.WireDisconnectTime.Count > (int)AllowedWireDisconnectionsPerMinute)
{
clientMemory.WireDisconnectTime.RemoveRange(0, clientMemory.WireDisconnectTime.Count - (int)AllowedWireDisconnectionsPerMinute);
if (clientMemory.WireDisconnectTime.All(w => Timing.TotalTime - w.Second < 60.0f))
{
float karmaDecrease = -WireDisconnectionKarmaDecrease;
//engineers don't lose as much karma for removing lots of wires
if (client.Character.Info?.Job.Prefab.Identifier == "engineer") { karmaDecrease *= 0.5f; }
AdjustKarma(client.Character, karmaDecrease, "Disconnected excessive number of wires");
}
}
if (client.Character?.Info?.Job.Prefab.Identifier == "captain" && client.Character.SelectedConstruction != null)
{
if (client.Character.SelectedConstruction.GetComponent<Steering>() != null)
{
AdjustKarma(client.Character, SteerSubKarmaIncrease * deltaTime, "Steering the sub");
}
}
}
if (client.Karma < KickBanThreshold && client.Connection != GameMain.Server.OwnerConnection)
{
if (TestMode)
{
client.Karma = 50.0f;
GameMain.Server.SendDirectChatMessage("BANNED! (not really because karma test mode is enabled)", client);
}
else
{
bannedClients.Add(client);
}
}
}
public void OnClientDisconnected(Client client)
{
clientMemories.Remove(client);
}
public void OnCharacterHealthChanged(Character target, Character attacker, float damage, IEnumerable<Affliction> appliedAfflictions = null)
{
if (target == null || attacker == null) { return; }
if (target == attacker) { return; }
//damaging dead characters doesn't affect karma
if (target.IsDead || target.Removed) { return; }
bool isEnemy = target.AIController is EnemyAIController || target.TeamID != attacker.TeamID;
if (GameMain.Server.TraitorManager != null)
{
if (GameMain.Server.TraitorManager.TraitorList.Any(t => t.Character == target))
{
//traitors always count as enemies
isEnemy = true;
}
if (GameMain.Server.TraitorManager.TraitorList.Any(t => t.Character == attacker && t.TargetCharacter == target))
{
//target counts as an enemy to the traitor
isEnemy = true;
}
}
if (appliedAfflictions != null)
{
foreach (Affliction affliction in appliedAfflictions)
{
if (MathUtils.NearlyEqual(affliction.Prefab.KarmaChangeOnApplied, 0.0f)) { continue; }
damage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
}
if (target.AIController is EnemyAIController || target.TeamID != attacker.TeamID)
{
if (damage > 0)
{
float karmaIncrease = damage * DamageEnemyKarmaIncrease;
if (attacker?.Info?.Job.Prefab.Identifier == "securityofficer") { karmaIncrease *= 2.0f; }
AdjustKarma(attacker, karmaIncrease, "Damaged enemy");
}
}
else
{
if (damage > 0)
{
AdjustKarma(attacker, -damage * DamageFriendlyKarmaDecrease, "Damaged friendly");
}
else
{
float karmaIncrease = -damage * HealFriendlyKarmaIncrease;
if (attacker?.Info?.Job.Prefab.Identifier == "medicaldoctor") { karmaIncrease *= 2.0f; }
AdjustKarma(attacker, karmaIncrease, "Healed friendly");
}
}
}
public void OnStructureHealthChanged(Structure structure, Character attacker, float damageAmount)
{
if (attacker == null) { return; }
//damaging/repairing ruin structures or enemy subs doesn't affect karma
if (structure.Submarine == null || structure.Submarine.TeamID != attacker.TeamID)
{
return;
}
if (damageAmount > 0)
{
if (StructureDamageKarmaDecrease <= 0.0f) { return; }
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker);
if (client != null)
{
//cap the damage so the karma can't decrease by more than MaxStructureDamageKarmaDecreasePerSecond per second
var clientMemory = GetClientMemory(client);
clientMemory.StructureDamageAccumulator += damageAmount;
if (clientMemory.StructureDamagePerSecond + damageAmount >= MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease)
{
damageAmount -= (MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease) - clientMemory.StructureDamagePerSecond;
if (damageAmount <= 0.0f) { return; }
}
}
AdjustKarma(attacker, -damageAmount * StructureDamageKarmaDecrease, "Damaged structures");
}
else
{
float karmaIncrease = -damageAmount * StructureRepairKarmaIncrease;
//mechanics get twice as much karma for repairing walls
if (attacker.Info?.Job.Prefab.Identifier == "mechanic") { karmaIncrease *= 2.0f; }
AdjustKarma(attacker, karmaIncrease, "Repaired structures");
}
}
public void OnItemRepaired(Character character, Repairable repairable, float repairAmount)
{
float karmaIncrease = repairAmount * ItemRepairKarmaIncrease;
if (repairable.HasRequiredSkills(character)) { karmaIncrease *= 2.0f; }
AdjustKarma(character, karmaIncrease, "Repaired item");
}
public void OnReactorOverHeating(Character character, float deltaTime)
{
AdjustKarma(character, -ReactorOverheatKarmaDecrease * deltaTime, "Caused reactor to overheat");
}
public void OnReactorMeltdown(Character character)
{
AdjustKarma(character, -ReactorMeltdownKarmaDecrease, "Caused a reactor meltdown");
}
public void OnExtinguishingFire(Character character, float deltaTime)
{
AdjustKarma(character, ExtinguishFireKarmaIncrease * deltaTime, "Extinguished a fire");
}
public void OnWireDisconnected(Character character, Wire wire)
{
if (character == null || wire == null) { return; }
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (client == null) { return; }
if (!clientMemories.ContainsKey(client)) { clientMemories[client] = new ClientMemory(); }
clientMemories[client].WireDisconnectTime.RemoveAll(w => w.First == wire);
clientMemories[client].WireDisconnectTime.Add(new Pair<Wire, float>(wire, (float)Timing.TotalTime));
}
private ClientMemory GetClientMemory(Client client)
{
if (!clientMemories.ContainsKey(client))
{
clientMemories[client] = new ClientMemory()
{
PreviousNotifiedKarma = client.Karma
};
}
return clientMemories[client];
}
public void OnSpamFilterTriggered(Client client)
{
if (client != null)
{
client.Karma -= SpamFilterKarmaDecrease;
SendKarmaNotifications(client, "Triggered the spam filter");
}
}
private void AdjustKarma(Character target, float amount, string debugKarmaChangeReason = "")
{
if (target == null) { return; }
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == target);
if (client == null) { return; }
client.Karma += amount;
if (TestMode)
{
SendKarmaNotifications(client, debugKarmaChangeReason);
}
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -53,36 +54,34 @@ namespace Barotrauma.Networking
return botsToRespawn;
}
partial void UpdateWaiting(float deltaTime)
private bool RespawnPending()
{
int characterToRespawnCount = GetClientsToRespawn().Count;
int totalCharacterCount = GameMain.Server.ConnectedClients.Count;
/*if (server.Character != null)
{
totalCharacterCount++;
if (server.Character.IsDead) characterToRespawnCount++;
}*/
bool startCountdown = (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f);
return (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f);
}
if (startCountdown != CountdownStarted)
partial void UpdateWaiting(float deltaTime)
{
bool respawnPending = RespawnPending();
if (respawnPending != RespawnCountdownStarted)
{
CountdownStarted = startCountdown;
RespawnCountdownStarted = respawnPending;
RespawnTime = DateTime.Now + new TimeSpan(0,0,0,0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f));
GameMain.Server.CreateEntityEvent(this);
}
if (!CountdownStarted) return;
if (!RespawnCountdownStarted) { return; }
respawnTimer -= deltaTime;
if (respawnTimer <= 0.0f)
if (DateTime.Now > RespawnTime)
{
respawnTimer = GameMain.Server.ServerSettings.RespawnInterval;
DispatchShuttle();
RespawnCountdownStarted = false;
}
if (respawnShuttle == null) return;
if (RespawnShuttle == null) { return; }
respawnShuttle.Velocity = Vector2.Zero;
RespawnShuttle.Velocity = Vector2.Zero;
if (shuttleSteering != null)
{
@@ -93,9 +92,9 @@ namespace Barotrauma.Networking
partial void DispatchShuttle()
{
if (respawnShuttle != null)
if (RespawnShuttle != null)
{
state = State.Transporting;
CurrentState = State.Transporting;
GameMain.Server.CreateEntityEvent(this);
ResetShuttle();
@@ -110,11 +109,27 @@ namespace Barotrauma.Networking
RespawnCharacters();
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
Vector2 spawnPos = FindSpawnPos();
if (spawnPos.Y > Level.Loaded.Size.Y)
{
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
}
else
{
RespawnShuttle.SetPosition(spawnPos);
RespawnShuttle.Velocity = Vector2.Zero;
if (shuttleSteering != null)
{
shuttleSteering.AutoPilot = true;
shuttleSteering.MaintainPos = true;
shuttleSteering.PosToMaintain = RespawnShuttle.WorldPosition;
shuttleSteering.UnsentChanges = true;
}
}
}
else
{
state = State.Waiting;
CurrentState = State.Waiting;
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
@@ -129,18 +144,18 @@ namespace Barotrauma.Networking
if (door.IsOpen) door.TrySetState(false, false, true);
}
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == respawnShuttle && g.ConnectedWall != null);
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null);
shuttleGaps.ForEach(g => Spawner.AddToRemoveQueue(g));
var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == respawnShuttle && i.GetComponent<DockingPort>() != null);
var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == RespawnShuttle && i.GetComponent<DockingPort>() != null);
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
//shuttle has returned if the path has been traversed or the shuttle is close enough to the exit
if (!CoroutineManager.IsCoroutineRunning("forcepos"))
{
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))
|| (RespawnShuttle.WorldPosition.Y + RespawnShuttle.Borders.Y > Level.Loaded.StartPosition.Y - Level.ShaftHeight &&
Math.Abs(Level.Loaded.StartPosition.X - RespawnShuttle.WorldPosition.X) < 1000.0f))
{
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(
@@ -149,46 +164,60 @@ namespace Barotrauma.Networking
}
}
if (respawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || shuttleReturnTimer <= 0.0f)
if (RespawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || DateTime.Now > despawnTime)
{
CoroutineManager.StopCoroutines("forcepos");
ResetShuttle();
state = State.Waiting;
CurrentState = State.Waiting;
GameServer.Log("The respawn shuttle has left.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
respawnTimer = GameMain.Server.ServerSettings.RespawnInterval;
CountdownStarted = false;
RespawnCountdownStarted = false;
}
}
partial void UpdateTransportingProjSpecific(float deltaTime)
{
//if there are no living chracters inside, transporting can be stopped immediately
if (!Character.CharacterList.Any(c => c.Submarine == respawnShuttle && !c.IsDead))
if (!ReturnCountdownStarted)
{
shuttleTransportTimer = 0.0f;
//if there are no living chracters inside, transporting can be stopped immediately
if (!Character.CharacterList.Any(c => c.Submarine == RespawnShuttle && !c.IsDead))
{
ReturnTime = DateTime.Now;
ReturnCountdownStarted = true;
}
else if (!RespawnPending())
{
//don't start counting down until someone else needs to respawn
ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000));
despawnTime = ReturnTime + new TimeSpan(0, 0, seconds: 30);
return;
}
else
{
ReturnCountdownStarted = true;
GameMain.Server.CreateEntityEvent(this);
}
}
if (shuttleTransportTimer <= 0.0f)
if (DateTime.Now > ReturnTime)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
state = State.Returning;
CurrentState = State.Returning;
GameMain.Server.CreateEntityEvent(this);
CountdownStarted = false;
RespawnCountdownStarted = false;
maxTransportTime = GameMain.Server.ServerSettings.MaxTransportTime;
shuttleReturnTimer = maxTransportTime;
shuttleTransportTimer = maxTransportTime;
}
}
partial void RespawnCharactersProjSpecific()
{
var respawnSub = respawnShuttle ?? Submarine.MainSub;
var respawnSub = RespawnShuttle ?? Submarine.MainSub;
var clients = GetClientsToRespawn();
foreach (Client c in clients)
@@ -250,7 +279,7 @@ namespace Barotrauma.Networking
GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.RemoteEndPoint?.Address, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
}
if (divingSuitPrefab != null && oxyPrefab != null && respawnShuttle != null)
if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null)
{
Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position;
if (divingSuitPrefab != null && oxyPrefab != null)
@@ -295,5 +324,27 @@ namespace Barotrauma.Networking
}
}
}
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
{
msg.WriteRangedInteger(0, Enum.GetNames(typeof(State)).Length, (int)CurrentState);
switch (CurrentState)
{
case State.Transporting:
msg.Write(ReturnCountdownStarted);
msg.Write(GameMain.Server.ServerSettings.MaxTransportTime);
msg.Write((float)(ReturnTime - DateTime.Now).TotalSeconds);
break;
case State.Waiting:
msg.Write(RespawnCountdownStarted);
msg.Write((float)(RespawnTime - DateTime.Now).TotalSeconds);
break;
case State.Returning:
break;
}
msg.WritePadBits();
}
}
}
@@ -101,8 +101,12 @@ namespace Barotrauma.Networking
if (netProperties.ContainsKey(key))
{
object prevValue = netProperties[key].Value;
netProperties[key].Read(incMsg);
GameServer.Log(c.Name + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage);
if (!netProperties[key].PropEquals(prevValue, netProperties[key]))
{
GameServer.Log(c.Name + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage);
}
changed = true;
}
else
@@ -205,6 +209,12 @@ namespace Barotrauma.Networking
{
doc.Save(writer);
}
if (KarmaPreset == "custom")
{
GameMain.Server?.KarmaManager?.SaveCustomPreset();
}
GameMain.Server?.KarmaManager?.Save();
}
private void LoadSettings()
@@ -300,9 +310,6 @@ namespace Barotrauma.Networking
{
if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true);
}
AutoBanTime = doc.Root.GetAttributeFloat("autobantime", 60);
MaxAutoBanTime = doc.Root.GetAttributeFloat("maxautobantime", 360);
}
public void LoadClientPermissions()
@@ -89,6 +89,17 @@ namespace Barotrauma
sb.AppendLine(exception.StackTrace);
sb.AppendLine("\n");
if (exception.InnerException != null)
{
sb.AppendLine("InnerException: " + exception.InnerException.Message);
if (exception.InnerException.TargetSite != null)
{
sb.AppendLine("Target site: " + exception.InnerException.TargetSite.ToString());
}
sb.AppendLine("Stack trace: ");
sb.AppendLine(exception.InnerException.StackTrace);
}
sb.AppendLine("Last debug messages:");
for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i-- )
{