(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,204 @@
using Microsoft.Xna.Framework;
using System;
//TODO: this class still does things that the server doesn't need, cleanup
namespace Barotrauma
{
public class Camera
{
public static Camera Instance = new Camera();
public static bool FollowSub = true;
const float DefaultZoom = 1.0f;
const float ZoomSmoothness = 8.0f;
const float MoveSmoothness = 8.0f;
private float zoom;
private float offsetAmount;
private Matrix transform, shaderTransform, viewMatrix;
private Vector2 position;
private float rotation;
private Vector2 prevPosition;
private float prevZoom;
public float Shake;
private Vector2 shakePosition;
private Vector2 shakeTargetPosition;
//the area of the world inside the camera view
private Rectangle worldView;
private Point resolution;
private Vector2 targetPos;
public float Zoom
{
get { return zoom; }
set
{
zoom = value;
Vector2 center = WorldViewCenter;
float newWidth = resolution.X / zoom;
float newHeight = resolution.Y / zoom;
worldView = new Rectangle(
(int)(center.X - newWidth / 2.0f),
(int)(center.Y + newHeight / 2.0f),
(int)newWidth,
(int)newHeight);
//UpdateTransform();
}
}
public float Rotation
{
get { return rotation; }
set { rotation = value; }
}
public float OffsetAmount
{
get { return offsetAmount; }
set { offsetAmount = value; }
}
public Point Resolution
{
get { return resolution; }
}
public Rectangle WorldView
{
get { return worldView; }
}
public Vector2 WorldViewCenter
{
get
{
return new Vector2(
worldView.X + worldView.Width / 2.0f,
worldView.Y - worldView.Height / 2.0f);
}
}
public Matrix Transform
{
get { return transform; }
}
public Matrix ShaderTransform
{
get { return shaderTransform; }
}
public Camera()
{
zoom = 1.0f;
rotation = 0.0f;
position = Vector2.Zero;
worldView = new Rectangle(0,0,
1,
1);
resolution = new Point(1,1);
viewMatrix =
Matrix.CreateTranslation(new Vector3(0.5f, 0.5f, 0));
UpdateTransform();
}
public Vector2 TargetPos
{
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)
{
Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position;
float interpolatedZoom = interpolate ? Timing.Interpolate(prevZoom, zoom) : zoom;
worldView.X = (int)(interpolatedPosition.X - worldView.Width / 2.0);
worldView.Y = (int)(interpolatedPosition.Y + worldView.Height / 2.0);
if (Level.Loaded != null && clampPos)
{
position.Y -= Math.Max(worldView.Y - Level.Loaded.Size.Y, 0.0f);
interpolatedPosition.Y -= Math.Max(worldView.Y - Level.Loaded.Size.Y, 0.0f);
worldView.Y = Math.Min((int)Level.Loaded.Size.Y, worldView.Y);
}
transform = Matrix.CreateTranslation(
new Vector3(-interpolatedPosition.X, interpolatedPosition.Y, 0)) *
Matrix.CreateScale(new Vector3(interpolatedZoom, interpolatedZoom, 1)) *
viewMatrix;
shaderTransform = Matrix.CreateTranslation(
new Vector3(
-interpolatedPosition.X - resolution.X / interpolatedZoom / 2.0f,
-interpolatedPosition.Y - resolution.Y / interpolatedZoom / 2.0f, 0)) *
Matrix.CreateScale(new Vector3(interpolatedZoom, interpolatedZoom, 1)) *
viewMatrix;
if (!interpolate)
{
prevPosition = position;
prevZoom = zoom;
}
}
public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true)
{
return;
}
public Vector2 Position
{
get { return position; }
set
{
if (!MathUtils.IsValid(value))
{
return;
}
position = value;
}
}
public Vector2 ScreenToWorld(Vector2 coords)
{
Vector2 worldCoords = Vector2.Transform(coords, Matrix.Invert(transform));
return new Vector2(worldCoords.X, -worldCoords.Y);
}
public Vector2 WorldToScreen(Vector2 coords)
{
coords.Y = -coords.Y;
//Vector2 screenCoords = Vector2.Transform(coords, transform);
return Vector2.Transform(coords, transform);
}
}
}
@@ -0,0 +1,39 @@
using Barotrauma.Networking;
using System.Xml.Linq;
namespace Barotrauma
{
partial class Character
{
public static Character Controlled = null;
partial void InitProjSpecific(XElement mainElement) { }
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult)
{
GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, attackResult.Afflictions);
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction)
{
if (causeOfDeath == CauseOfDeathType.Affliction)
{
GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack);
}
else
{
GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack);
}
healthUpdateTimer = 0.0f;
foreach (Client client in GameMain.Server.ConnectedClients)
{
if (client.InGame)
{
client.PendingPositionUpdates.Enqueue(this);
}
}
}
}
}
@@ -0,0 +1,39 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class CharacterInfo
{
public void ServerWrite(IWriteMessage msg)
{
msg.Write(ID);
msg.Write(Name);
msg.Write((byte)Gender);
msg.Write((byte)Race);
msg.Write((byte)HeadSpriteId);
msg.Write((byte)Head.HairIndex);
msg.Write((byte)Head.BeardIndex);
msg.Write((byte)Head.MoustacheIndex);
msg.Write((byte)Head.FaceAttachmentIndex);
msg.Write(ragdollFileName);
if (Job != null)
{
msg.Write(Job.Prefab.Identifier);
msg.Write((byte)Job.Variant);
msg.Write((byte)Job.Skills.Count);
foreach (Skill skill in Job.Skills)
{
msg.Write(skill.Identifier);
msg.Write(skill.Level);
}
}
else
{
msg.Write("");
msg.Write((byte)0);
}
// TODO: animations
}
}
}
@@ -0,0 +1,546 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Character
{
public string OwnerClientEndPoint;
public string OwnerClientName;
public bool ClientDisconnected;
public float KillDisconnectedTimer;
private bool networkUpdateSent;
private double LastInputTime;
public float GetPositionUpdateInterval(Client recipient)
{
if (!Enabled) { return 1000.0f; }
Vector2 comparePosition = recipient.SpectatePos == null ? recipient.Character.WorldPosition : recipient.SpectatePos.Value;
float distance = Vector2.Distance(comparePosition, WorldPosition);
float priority = 1.0f - MathUtils.InverseLerp(
NetConfig.HighPrioCharacterPositionUpdateDistance,
NetConfig.LowPrioCharacterPositionUpdateDistance,
distance);
float interval = MathHelper.Lerp(
NetConfig.LowPrioCharacterPositionUpdateInterval,
NetConfig.HighPrioCharacterPositionUpdateInterval,
priority);
if (IsDead)
{
interval = Math.Max(interval * 2, 0.1f);
}
return interval;
}
partial void UpdateNetInput()
{
if (!(this is AICharacter) || IsRemotePlayer)
{
if (!CanMove)
{
AnimController.Frozen = false;
if (memInput.Count > 0)
{
prevDequeuedInput = dequeuedInput;
dequeuedInput = memInput[memInput.Count - 1].states;
memInput.RemoveAt(memInput.Count - 1);
}
}
else if (memInput.Count == 0)
{
AnimController.Frozen = true;
if (Timing.TotalTime > LastInputTime + 0.5)
{
//no inputs have been received in 0.5 seconds, reset input
//(if there's a temporary network hiccup that prevents us from receiving inputs, we assume the inputs haven't changed,
//but if it takes too long, for example due to a client crashing/disconnecting, we don't want to keep the character
//firing a welding tool or whatever else they were doing until the kill disconnect timer kicks in)
prevDequeuedInput = dequeuedInput =
dequeuedInput.HasFlag(InputNetFlags.FacingLeft) ? InputNetFlags.FacingLeft : InputNetFlags.None;
}
}
else
{
AnimController.Frozen = false;
prevDequeuedInput = dequeuedInput;
LastProcessedID = memInput[memInput.Count - 1].networkUpdateID;
dequeuedInput = memInput[memInput.Count - 1].states;
double aimAngle = ((double)memInput[memInput.Count - 1].intAim / 65535.0) * 2.0 * Math.PI;
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f;
//reset focus when attempting to use/select something
if (memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Use) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Deselect) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Health) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Grab))
{
focusedItem = null;
FocusedCharacter = null;
}
var closestEntity = FindEntityByID(memInput[memInput.Count - 1].interact);
if (closestEntity is Item)
{
if (CanInteractWith((Item)closestEntity))
{
focusedItem = (Item)closestEntity;
FocusedCharacter = null;
}
}
else if (closestEntity is Character)
{
if (CanInteractWith((Character)closestEntity))
{
FocusedCharacter = (Character)closestEntity;
focusedItem = null;
}
}
memInput.RemoveAt(memInput.Count - 1);
TransformCursorPos();
if ((dequeuedInput == InputNetFlags.None || dequeuedInput == InputNetFlags.FacingLeft) && Math.Abs(AnimController.Collider.LinearVelocity.X) < 0.005f && Math.Abs(AnimController.Collider.LinearVelocity.Y) < 0.2f)
{
while (memInput.Count > 5 && memInput[memInput.Count - 1].states == dequeuedInput)
{
//remove inputs where the player is not moving at all
//helps the server catch up, shouldn't affect final position
LastProcessedID = memInput[memInput.Count - 1].networkUpdateID;
memInput.RemoveAt(memInput.Count - 1);
}
}
}
}
AnimController.Frozen = false;
if (networkUpdateSent)
{
foreach (Key key in keys)
{
key.DequeueHit();
key.DequeueHeld();
}
networkUpdateSent = false;
}
}
public virtual void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
if (GameMain.Server == null) return;
switch (type)
{
case ClientNetObject.CHARACTER_INPUT:
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
UInt16 networkUpdateID = msg.ReadUInt16();
byte inputCount = msg.ReadByte();
if (AllowInput) { Enabled = true; }
for (int i = 0; i < inputCount; i++)
{
InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal);
UInt16 newAim = 0;
UInt16 newInteract = 0;
if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft)
{
c.KickAFKTimer = 0.0f;
}
else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft))
{
//character changed the direction they're facing
c.KickAFKTimer = 0.0f;
}
newAim = msg.ReadUInt16();
if (newInput.HasFlag(InputNetFlags.Select) ||
newInput.HasFlag(InputNetFlags.Deselect) ||
newInput.HasFlag(InputNetFlags.Use) ||
newInput.HasFlag(InputNetFlags.Health) ||
newInput.HasFlag(InputNetFlags.Grab))
{
newInteract = msg.ReadUInt16();
}
if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60))
{
if ((i > 0 && memInput[i - 1].intAim != newAim))
{
c.KickAFKTimer = 0.0f;
}
NetInputMem newMem = new NetInputMem
{
states = newInput,
intAim = newAim,
interact = newInteract,
networkUpdateID = (ushort)(networkUpdateID - i)
};
memInput.Insert(i, newMem);
LastInputTime = Timing.TotalTime;
}
}
if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID))
{
LastNetworkUpdateID = networkUpdateID;
}
if (memInput.Count > 60)
{
//deleting inputs from the queue here means the server is way behind and data needs to be dropped
//we'll make the server drop down to 30 inputs for good measure
memInput.RemoveRange(30, memInput.Count - 30);
}
break;
case ClientNetObject.ENTITY_STATE:
int eventType = msg.ReadRangedInteger(0, 3);
switch (eventType)
{
case 0:
Inventory.ServerRead(type, msg, c);
break;
case 1:
bool doingCPR = msg.ReadBoolean();
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None;
break;
case 2:
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
if (IsUnconscious)
{
var causeOfDeath = CharacterHealth.GetCauseOfDeath();
Kill(causeOfDeath.First, causeOfDeath.Second);
}
break;
}
break;
}
msg.ReadPadBits();
}
public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
if (GameMain.Server == null) return;
if (extraData != null)
{
switch ((NetEntityEvent.Type)extraData[0])
{
case NetEntityEvent.Type.InventoryState:
msg.WriteRangedInteger(0, 0, 3);
msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
Inventory.ServerWrite(msg, c);
break;
case NetEntityEvent.Type.Control:
msg.WriteRangedInteger(1, 0, 3);
Client owner = (Client)extraData[1];
msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0);
break;
case NetEntityEvent.Type.Status:
msg.WriteRangedInteger(2, 0, 3);
WriteStatus(msg);
break;
case NetEntityEvent.Type.UpdateSkills:
msg.WriteRangedInteger(3, 0, 3);
if (Info?.Job == null)
{
msg.Write((byte)0);
}
else
{
msg.Write((byte)Info.Job.Skills.Count);
foreach (Skill skill in Info.Job.Skills)
{
msg.Write(skill.Identifier);
msg.Write(skill.Level);
}
}
break;
default:
DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")");
break;
}
msg.WritePadBits();
}
else
{
msg.Write(ID);
IWriteMessage tempBuffer = new WriteOnlyMessage();
if (this == c.Character)
{
tempBuffer.Write(true);
if (LastNetworkUpdateID < memInput.Count + 1)
{
tempBuffer.Write((UInt16)0);
}
else
{
tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1));
}
}
else
{
tempBuffer.Write(false);
bool aiming = false;
bool use = false;
bool attack = false;
bool shoot = false;
if (IsRemotePlayer)
{
aiming = dequeuedInput.HasFlag(InputNetFlags.Aim);
use = dequeuedInput.HasFlag(InputNetFlags.Use);
attack = dequeuedInput.HasFlag(InputNetFlags.Attack);
shoot = dequeuedInput.HasFlag(InputNetFlags.Shoot);
}
else if (keys != null)
{
aiming = keys[(int)InputType.Aim].GetHeldQueue;
use = keys[(int)InputType.Use].GetHeldQueue;
attack = keys[(int)InputType.Attack].GetHeldQueue;
shoot = keys[(int)InputType.Shoot].GetHeldQueue;
networkUpdateSent = true;
}
tempBuffer.Write(aiming);
tempBuffer.Write(shoot);
tempBuffer.Write(use);
if (AnimController is HumanoidAnimController)
{
tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching);
}
tempBuffer.Write(attack);
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI)));
tempBuffer.Write(IsRagdolled || IsUnconscious || Stun > 0.0f || IsDead);
tempBuffer.Write(AnimController.Dir > 0.0f);
}
if (SelectedCharacter != null || SelectedConstruction != null)
{
tempBuffer.Write(true);
tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID);
tempBuffer.Write(SelectedConstruction != null ? SelectedConstruction.ID : NullEntityID);
if (SelectedCharacter != null)
{
tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR);
}
}
else
{
tempBuffer.Write(false);
}
tempBuffer.Write(SimPosition.X);
tempBuffer.Write(SimPosition.Y);
float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
AnimController.Collider.LinearVelocity = new Vector2(
MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel),
MathHelper.Clamp(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel));
tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12);
tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12);
bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled;
tempBuffer.Write(fixedRotation);
if (!fixedRotation)
{
tempBuffer.Write(AnimController.Collider.Rotation);
float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity;
AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8);
tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8);
}
bool writeStatus = healthUpdateTimer <= 0.0f;
tempBuffer.Write(writeStatus);
if (writeStatus)
{
WriteStatus(tempBuffer);
}
tempBuffer.WritePadBits();
msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes);
msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
}
}
private void WriteStatus(IWriteMessage msg)
{
msg.Write(IsDead);
if (IsDead)
{
msg.WriteRangedInteger((int)CauseOfDeath.Type, 0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1);
if (CauseOfDeath.Type == CauseOfDeathType.Affliction)
{
msg.Write(CauseOfDeath.Affliction.Identifier);
}
if (AnimController?.LimbJoints == null)
{
//0 limbs severed
msg.Write((byte)0);
}
else
{
List<int> severedJointIndices = new List<int>();
for (int i = 0; i < AnimController.LimbJoints.Length; i++)
{
if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered)
{
severedJointIndices.Add(i);
}
}
msg.Write((byte)severedJointIndices.Count);
foreach (int jointIndex in severedJointIndices)
{
msg.Write((byte)jointIndex);
}
}
}
else
{
CharacterHealth.ServerWrite(msg);
}
}
public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMessageSize)
{
if (GameMain.Server == null) return;
int msgLength = msg.LengthBytes;
msg.Write(Info == null);
msg.Write(entityId);
msg.Write(SpeciesName);
msg.Write(seed);
if (Removed)
{
msg.Write(0.0f);
msg.Write(0.0f);
}
else
{
msg.Write(WorldPosition.X);
msg.Write(WorldPosition.Y);
}
msg.Write(Enabled);
//character with no characterinfo (e.g. some monster)
if (Info == null)
{
TryWriteStatus(msg);
return;
}
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (ownerClient != null)
{
msg.Write(true);
msg.Write(ownerClient.ID);
}
else if (GameMain.Server.Character == this)
{
msg.Write(true);
msg.Write((byte)0);
}
else
{
msg.Write(false);
}
msg.Write((byte)TeamID);
msg.Write(this is AICharacter);
msg.Write(info.SpeciesName);
info.ServerWrite(msg);
// Current order
if (info.CurrentOrder != null)
{
msg.Write(true);
msg.Write((byte)Order.PrefabList.IndexOf(info.CurrentOrder.Prefab));
msg.Write(info.CurrentOrder.TargetEntity == null ? (UInt16)0 :
info.CurrentOrder.TargetEntity.ID);
if (info.CurrentOrder.OrderGiver != null)
{
msg.Write(true);
msg.Write(info.CurrentOrder.OrderGiver.ID);
}
else
{
msg.Write(false);
}
msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 :
Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption)));
}
else
{
msg.Write(false);
}
TryWriteStatus(msg);
void TryWriteStatus(IWriteMessage msg)
{
var tempBuffer = new ReadWriteMessage();
WriteStatus(tempBuffer);
if (msg.LengthBytes + tempBuffer.LengthBytes >= 255 && restrictMessageSize)
{
msg.Write(false);
DebugConsole.ThrowError($"Error when writing character spawn data: status data caused the length of the message to exceed 255 bytes ({msg.LengthBytes} + {tempBuffer.LengthBytes})");
}
else
{
msg.Write(true);
WriteStatus(msg);
}
}
DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - msgLength));
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class CargoMission : Mission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
msg.Write((ushort)items.Count);
foreach (Item item in items)
{
item.WriteSpawnData(msg, item.ID);
}
}
}
}
@@ -0,0 +1,117 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class CombatMission
{
private readonly bool[] teamDead = new bool[2];
private bool initialized = false;
public override string Description
{
get
{
if (descriptions == null) return "";
//non-team-specific description
return descriptions[0];
}
}
public override void AssignTeamIDs(List<Client> clients)
{
List<Client> randList = new List<Client>(clients);
for (int i = 0; i < randList.Count; i++)
{
Client a = randList[i];
int oi = Rand.Range(0, randList.Count - 1);
Client b = randList[oi];
randList[i] = b;
randList[oi] = a;
}
int halfPlayers = randList.Count / 2;
for (int i = 0; i < randList.Count; i++)
{
if (i < halfPlayers)
{
randList[i].TeamID = Character.TeamType.Team1;
}
else
{
randList[i].TeamID = Character.TeamType.Team2;
}
}
}
public override void Update(float deltaTime)
{
if (!initialized)
{
crews[0].Clear();
crews[1].Clear();
foreach (Character character in Character.CharacterList)
{
if (character.TeamID == Character.TeamType.Team1)
{
crews[0].Add(character);
}
else if (character.TeamID == Character.TeamType.Team2)
{
crews[1].Add(character);
}
}
initialized = true;
}
if (crews[0].Count == 0 || crews[1].Count == 0)
{
//if there are no characters in either crew, end the round
teamDead[0] = teamDead[1] = true;
state = 1;
}
else
{
teamDead[0] = crews[0].All(c => c.IsDead || c.IsUnconscious);
teamDead[1] = crews[1].All(c => c.IsDead || c.IsUnconscious);
}
if (state == 0)
{
for (int i = 0; i < teamDead.Length; i++)
{
if (!teamDead[i] && teamDead[1 - i])
{
//make sure nobody in the other team can be revived because that would be pretty weird
crews[1 - i].ForEach(c => { if (!c.IsDead) c.Kill(CauseOfDeathType.Unknown, null); });
GameMain.GameSession.WinningTeam = i == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2;
state = 1;
break;
}
}
}
else
{
if (teamDead[0] && teamDead[1])
{
GameMain.GameSession.WinningTeam = Character.TeamType.None;
if (GameMain.Server != null) { GameMain.Server.EndGame(); }
}
else if (GameMain.GameSession.WinningTeam != Character.TeamType.None)
{
GameMain.Server.EndGame();
}
}
}
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
//do nothing
}
}
}
@@ -0,0 +1,19 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class Mission
{
partial void ShowMessageProjSpecific(int missionState)
{
if (missionState >= Headers.Count && missionState >= Messages.Count) return;
string header = missionState < Headers.Count ? Headers[missionState] : "";
string message = missionState < Messages.Count ? Messages[missionState] : "";
GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage);
}
public abstract void ServerWriteInitial(IWriteMessage msg, Client c);
}
}
@@ -0,0 +1,22 @@
using Barotrauma.Networking;
using System;
namespace Barotrauma
{
partial class MonsterMission : Mission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
if (monsters.Count == 0 && monsterFiles.Count > 0)
{
throw new InvalidOperationException("Server attempted to write monster mission data when no monsters had been spawned.");
}
msg.Write((byte)monsters.Count);
foreach (Character monster in monsters)
{
monster.WriteSpawnData(msg, monster.ID, restrictMessageSize: false);
}
}
}
}
@@ -0,0 +1,12 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class SalvageMission : Mission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
item.WriteSpawnData(msg, item.ID);
}
}
}
@@ -0,0 +1,398 @@
using Barotrauma.Networking;
using Barotrauma.Steam;
using FarseerPhysics.Dynamics;
using GameAnalyticsSDK.Net;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
{
class GameMain
{
public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version;
public static World World;
public static GameSettings Config;
public static GameServer Server;
public static NetworkMember NetworkMember
{
get { return Server as NetworkMember; }
}
public static GameSession GameSession;
public static GameMain Instance
{
get;
private set;
}
//only screens the server implements
public static GameScreen GameScreen;
public static NetLobbyScreen NetLobbyScreen;
//null screens because they are not implemented by the server,
//but they're checked for all over the place
//TODO: maybe clean up instead of having these constants
public static readonly Screen SubEditorScreen = UnimplementedScreen.Instance;
public static bool ShouldRun = true;
private static Stopwatch stopwatch;
public static IEnumerable<ContentPackage> SelectedPackages
{
get { return Config?.SelectedContentPackages; }
}
private static ContentPackage vanillaContent;
public static ContentPackage VanillaContent
{
get
{
if (vanillaContent == null)
{
// TODO: Dynamic method for defining and finding the vanilla content package.
vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase));
}
return vanillaContent;
}
}
public readonly string[] CommandLineArgs;
public GameMain(string[] args)
{
Instance = this;
CommandLineArgs = args;
World = new World(new Vector2(0, -9.82f));
FarseerPhysics.Settings.AllowSleep = true;
FarseerPhysics.Settings.ContinuousPhysics = false;
FarseerPhysics.Settings.VelocityIterations = 1;
FarseerPhysics.Settings.PositionIterations = 1;
Console.WriteLine("Loading game settings");
Config = new GameSettings();
Console.WriteLine("Loading MD5 hash cache");
Md5Hash.LoadCache();
Console.WriteLine("Initializing SteamManager");
SteamManager.Initialize();
Console.WriteLine("Initializing GameAnalytics");
if (GameSettings.SendUserStatistics) GameAnalyticsManager.Init();
Console.WriteLine("Initializing GameScreen");
GameScreen = new GameScreen();
}
public void Init()
{
CharacterPrefab.LoadAll();
MissionPrefab.Init();
TraitorMissionPrefab.Init();
MapEntityPrefab.Init();
MapGenerationParams.Init();
LevelGenerationParams.LoadPresets();
ScriptedEventSet.LoadPrefabs();
AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions));
SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings));
StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure));
ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item));
JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs));
NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations));
ItemAssemblyPrefab.LoadAll();
LevelObjectPrefab.LoadAll();
GameModePreset.Init();
LocationType.Init();
Submarine.RefreshSavedSubs();
Screen.SelectNull();
NetLobbyScreen = new NetLobbyScreen();
CheckContentPackage();
}
private void CheckContentPackage()
{
foreach (ContentPackage contentPackage in Config.SelectedContentPackages)
{
var exePaths = contentPackage.GetFilesOfType(ContentType.ServerExecutable);
if (exePaths.Count() > 0 && AppDomain.CurrentDomain.FriendlyName != exePaths.First())
{
DebugConsole.NewMessage(AppDomain.CurrentDomain.FriendlyName);
DebugConsole.ShowQuestionPrompt(TextManager.GetWithVariables("IncorrectExe", new string[2] { "[selectedpackage]", "[exename]" }, new string[2] { contentPackage.Name, exePaths.First() }),
(option) =>
{
if (option.ToLower() == "y" || option.ToLower() == "yes")
{
string fullPath = Path.GetFullPath(exePaths.First());
ToolBox.OpenFileWithShell(fullPath);
ShouldRun = false;
}
});
break;
}
}
}
/// <summary>
/// Returns the file paths of all files of the given type in the content packages.
/// </summary>
/// <param name="type"></param>
/// <param name="searchAllContentPackages">If true, also returns files in content packages that are installed but not currently selected.</param>
public IEnumerable<ContentFile> GetFilesOfType(ContentType type, bool searchAllContentPackages = false)
{
if (searchAllContentPackages)
{
return ContentPackage.GetFilesOfType(ContentPackage.List, type);
}
else
{
return ContentPackage.GetFilesOfType(SelectedPackages, type);
}
}
public void StartServer()
{
string name = "Server";
int port = NetConfig.DefaultPort;
int queryPort = NetConfig.DefaultQueryPort;
bool publiclyVisible = false;
string password = "";
bool enableUpnp = false;
int maxPlayers = 10;
int ownerKey = 0;
UInt64 steamId = 0;
XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile);
if (doc?.Root == null)
{
DebugConsole.ThrowError("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings.");
}
else
{
name = doc.Root.GetAttributeString("name", "Server");
port = doc.Root.GetAttributeInt("port", NetConfig.DefaultPort);
queryPort = doc.Root.GetAttributeInt("queryport", NetConfig.DefaultQueryPort);
publiclyVisible = doc.Root.GetAttributeBool("public", false);
password = doc.Root.GetAttributeString("password", "");
enableUpnp = doc.Root.GetAttributeBool("enableupnp", false);
maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10);
ownerKey = 0;
}
#if DEBUG
foreach (string s in CommandLineArgs)
{
Console.WriteLine(s);
}
#endif
for (int i = 0; i < CommandLineArgs.Length; i++)
{
switch (CommandLineArgs[i].Trim())
{
case "-name":
name = CommandLineArgs[i + 1];
i++;
break;
case "-port":
int.TryParse(CommandLineArgs[i + 1], out port);
i++;
break;
case "-queryport":
int.TryParse(CommandLineArgs[i + 1], out queryPort);
i++;
break;
case "-public":
bool.TryParse(CommandLineArgs[i + 1], out publiclyVisible);
i++;
break;
case "-password":
password = CommandLineArgs[i + 1];
i++;
break;
case "-nopassword":
password = "";
break;
case "-upnp":
case "-enableupnp":
bool.TryParse(CommandLineArgs[i + 1], out enableUpnp);
i++;
break;
case "-maxplayers":
int.TryParse(CommandLineArgs[i + 1], out maxPlayers);
i++;
break;
case "-ownerkey":
int.TryParse(CommandLineArgs[i + 1], out ownerKey);
i++;
break;
case "-steamid":
UInt64.TryParse(CommandLineArgs[i + 1], out steamId);
i++;
break;
case "-pipes":
ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]);
i += 2;
break;
}
}
Server = new GameServer(
name,
port,
queryPort,
publiclyVisible,
password,
enableUpnp,
maxPlayers,
ownerKey,
steamId);
for (int i = 0; i < CommandLineArgs.Length; i++)
{
switch (CommandLineArgs[i].Trim())
{
case "-playstyle":
Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle);
Server.ServerSettings.PlayStyle = playStyle;
i++;
break;
case "-banafterwrongpassword":
bool.TryParse(CommandLineArgs[i + 1], out bool banAfterWrongPassword);
Server.ServerSettings.BanAfterWrongPassword = banAfterWrongPassword;
break;
case "-karma":
case "-karmaenabled":
bool.TryParse(CommandLineArgs[i + 1], out bool karmaEnabled);
Server.ServerSettings.KarmaEnabled = karmaEnabled;
i++;
break;
case "-karmapreset":
string karmaPresetName = CommandLineArgs[i + 1];
Server.ServerSettings.KarmaPreset = karmaPresetName;
i++;
break;
}
}
}
public void CloseServer()
{
Server?.Disconnect();
ShouldRun = false;
Server = null;
}
public void Run()
{
Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Character));
Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Item));
Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent));
Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull));
Init();
StartServer();
ResetFrameTime();
double frequency = (double)Stopwatch.Frequency;
if (frequency <= 1500)
{
DebugConsole.NewMessage("WARNING: Stopwatch frequency under 1500 ticks per second. Expect significant syncing accuracy issues.", Color.Yellow);
}
stopwatch = Stopwatch.StartNew();
long prevTicks = stopwatch.ElapsedTicks;
while (ShouldRun)
{
long currTicks = stopwatch.ElapsedTicks;
double elapsedTime = Math.Max(currTicks - prevTicks, 0) / frequency;
Timing.Accumulator += elapsedTime;
if (Timing.Accumulator > 1.0)
{
//prevent spiral of death
Timing.Accumulator = Timing.Step;
}
prevTicks = currTicks;
while (Timing.Accumulator >= Timing.Step)
{
Timing.TotalTime += Timing.Step;
DebugConsole.Update();
Screen.Selected?.Update((float)Timing.Step);
Server.Update((float)Timing.Step);
if (Server == null) { break; }
SteamManager.Update((float)Timing.Step);
TaskPool.Update();
CoroutineManager.Update((float)Timing.Step, (float)Timing.Step);
Timing.Accumulator -= Timing.Step;
}
#if !DEBUG
if (Server?.OwnerConnection == null && !Console.IsOutputRedirected)
{
DebugConsole.UpdateCommandLine((int)(Timing.Accumulator * 800));
}
else
{
DebugConsole.Clear();
}
#else
DebugConsole.UpdateCommandLine((int)(Timing.Accumulator * 800));
#endif
int frameTime = (int)(((double)(stopwatch.ElapsedTicks - prevTicks) / frequency) * 1000.0);
frameTime = Math.Max(0, frameTime);
Thread.Sleep(Math.Max(((int)(Timing.Step * 1000.0) - frameTime) / 2, 0));
}
stopwatch.Stop();
CloseServer();
SteamManager.ShutDown();
SaveUtil.CleanUnnecessarySaveFiles();
if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); }
if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); }
}
public static void ResetFrameTime()
{
Timing.Accumulator = 0.0f;
stopwatch?.Reset();
stopwatch?.Start();
}
public CoroutineHandle ShowLoading(IEnumerable<object> loader, bool waitKeyHit = true)
{
return CoroutineManager.StartCoroutine(loader);
}
public void Exit()
{
ShouldRun = false;
}
}
}
@@ -0,0 +1,23 @@
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma
{
partial class CrewManager
{
partial void CreateRandomConversation()
{
List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
c.AIController is HumanAIController &&
!c.IsDead &&
c.SpeechImpediment <= 100.0f);
foreach (Client client in GameMain.Server.ConnectedClients)
{
if (client.Character != null) availableSpeakers.Remove(client.Character);
}
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
}
}
}
@@ -0,0 +1,15 @@
using Barotrauma.Networking;
namespace Barotrauma
{
abstract partial class CampaignMode : GameMode
{
public override void ShowStartMessage()
{
if (Mission == null) return;
Networking.GameServer.Log(TextManager.Get("Mission") + ": " + Mission.Name, Networking.ServerLog.MessageType.ServerMessage);
Networking.GameServer.Log(Mission.Description, Networking.ServerLog.MessageType.ServerMessage);
}
}
}
@@ -0,0 +1,33 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class CharacterCampaignData
{
public bool HasSpawned;
partial void InitProjSpecific(Client client)
{
ClientEndPoint = client.Connection.EndPointString;
SteamID = client.SteamID;
CharacterInfo = client.CharacterInfo;
}
public bool MatchesClient(Client client)
{
if (SteamID > 0)
{
return SteamID == client.SteamID;
}
else
{
return ClientEndPoint == client.Connection.EndPointString;
}
}
public void SpawnInventoryItems(CharacterInfo characterInfo, Inventory inventory)
{
characterInfo.SpawnInventoryItems(inventory, itemData);
}
}
}
@@ -0,0 +1,13 @@
namespace Barotrauma
{
partial class MissionMode : GameMode
{
public override void ShowStartMessage()
{
if (mission == null) return;
Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage);
Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage);
}
}
}
@@ -0,0 +1,320 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class MultiPlayerCampaign : CampaignMode
{
private List<CharacterCampaignData> characterData = new List<CharacterCampaignData>();
public static void StartNewCampaign(string savePath, string subPath, string seed)
{
if (string.IsNullOrWhiteSpace(savePath)) return;
GameMain.GameSession = new GameSession(new Submarine(subPath, ""), savePath,
GameModePreset.List.Find(g => g.Identifier == "multiplayercampaign"));
var campaign = ((MultiPlayerCampaign)GameMain.GameSession.GameMode);
campaign.GenerateMap(seed);
campaign.SetDelegates();
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
GameMain.GameSession.Map.SelectRandomLocation(true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
campaign.LastSaveID++;
DebugConsole.NewMessage("Campaign started!", Color.Cyan);
DebugConsole.NewMessage(GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan);
}
public static void LoadCampaign(string selectedSave)
{
SaveUtil.LoadGame(selectedSave);
((MultiPlayerCampaign)GameMain.GameSession.GameMode).LastSaveID++;
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
GameMain.GameSession.Map.SelectRandomLocation(true);
DebugConsole.NewMessage("Campaign loaded!", Color.Cyan);
DebugConsole.NewMessage(GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan);
}
public static void StartCampaignSetup()
{
DebugConsole.NewMessage("********* CAMPAIGN SETUP *********", Color.White);
DebugConsole.ShowQuestionPrompt("Do you want to start a new campaign? Y/N", (string arg) =>
{
if (arg.Equals("y", StringComparison.OrdinalIgnoreCase) || arg.Equals("yes", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ShowQuestionPrompt("Enter a save name for the campaign:", (string saveName) =>
{
string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
StartNewCampaign(savePath, GameMain.NetLobbyScreen.SelectedSub.FilePath, GameMain.NetLobbyScreen.LevelSeed);
});
}
else
{
var saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer).ToArray();
if (saveFiles.Length == 0)
{
DebugConsole.ThrowError("No save files found.");
return;
}
DebugConsole.NewMessage("Saved campaigns:", Color.White);
for (int i = 0; i < saveFiles.Length; i++)
{
DebugConsole.NewMessage(" " + i + ". " + saveFiles[i], Color.White);
}
DebugConsole.ShowQuestionPrompt("Select a save file to load (0 - " + (saveFiles.Length - 1) + "):", (string selectedSave) =>
{
int saveIndex = -1;
if (!int.TryParse(selectedSave, out saveIndex)) { return; }
if (saveIndex < 0 || saveIndex >= saveFiles.Length)
{
DebugConsole.ThrowError("Invalid save file index.");
}
else
{
LoadCampaign(saveFiles[saveIndex]);
}
});
}
});
}
public bool AllowedToEndRound(Character interactor)
{
if (interactor == null || Level.Loaded?.StartOutpost == null || Level.Loaded?.EndOutpost == null)
{
return false;
}
if (interactor.Submarine == Level.Loaded.StartOutpost &&
interactor.CanInteractWith(startWatchman))
{
return true;
}
if (interactor.Submarine == Level.Loaded.EndOutpost &&
interactor.CanInteractWith(endWatchman))
{
return true;
}
return false;
}
protected override void WatchmanInteract(Character watchman, Character interactor)
{
if ((watchman.Submarine == Level.Loaded.StartOutpost && !Submarine.MainSub.AtStartPosition) ||
(watchman.Submarine == Level.Loaded.EndOutpost && !Submarine.MainSub.AtEndPosition))
{
CreateDialog(new List<Character> { watchman }, "WatchmanInteractNoLeavingSub", 5.0f);
return;
}
bool hasPermissions = true;
if (GameMain.Server != null)
{
var client = GameMain.Server.ConnectedClients.Find(c => c.Character == interactor);
hasPermissions = client != null;
CreateDialog(new List<Character> { watchman }, hasPermissions ? "WatchmanInteract" : "WatchmanInteractNotAllowed", 1.0f);
}
}
partial void SetDelegates()
{
if (GameMain.Server != null)
{
CargoManager.OnItemsChanged += () => { LastUpdateID++; };
Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; };
Map.OnMissionSelected += (loc, mission) => { LastUpdateID++; };
}
}
public void DiscardClientCharacterData(Client client)
{
characterData.RemoveAll(cd => cd.MatchesClient(client));
}
public CharacterCampaignData GetClientCharacterData(Client client)
{
return characterData.Find(cd => cd.MatchesClient(client));
}
public void AssignClientCharacterInfos(IEnumerable<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
if (client.SpectateOnly && GameMain.Server.ServerSettings.AllowSpectating) { continue; }
var matchingData = GetClientCharacterData(client);
if (matchingData != null) client.CharacterInfo = matchingData.CharacterInfo;
}
}
public Dictionary<Client, Job> GetAssignedJobs(IEnumerable<Client> connectedClients)
{
var assignedJobs = new Dictionary<Client, Job>();
foreach (Client client in connectedClients)
{
var matchingData = GetClientCharacterData(client);
if (matchingData != null) assignedJobs.Add(client, matchingData.CharacterInfo.Job);
}
return assignedJobs;
}
public void ServerWrite(IWriteMessage msg, Client c)
{
System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue);
msg.Write(CampaignID);
msg.Write(lastUpdateID);
msg.Write(lastSaveID);
msg.Write(map.Seed);
msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex);
msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex);
msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex);
msg.Write(isRunning && startWatchman != null ? startWatchman.ID : (UInt16)0);
msg.Write(isRunning && endWatchman != null ? endWatchman.ID : (UInt16)0);
msg.Write(Money);
msg.Write(PurchasedHullRepairs);
msg.Write(PurchasedItemRepairs);
msg.Write(PurchasedLostShuttles);
msg.Write((UInt16)CargoManager.PurchasedItems.Count);
foreach (PurchasedItem pi in CargoManager.PurchasedItems)
{
msg.Write(pi.ItemPrefab.Identifier);
msg.Write((UInt16)pi.Quantity);
}
var characterData = GetClientCharacterData(c);
if (characterData?.CharacterInfo == null)
{
msg.Write(false);
}
else
{
msg.Write(true);
characterData.CharacterInfo.ServerWrite(msg);
}
}
public void ServerRead(IReadMessage msg, Client sender)
{
UInt16 selectedLocIndex = msg.ReadUInt16();
byte selectedMissionIndex = msg.ReadByte();
bool purchasedHullRepairs = msg.ReadBoolean();
bool purchasedItemRepairs = msg.ReadBoolean();
bool purchasedLostShuttles = msg.ReadBoolean();
UInt16 purchasedItemCount = msg.ReadUInt16();
List<PurchasedItem> purchasedItems = new List<PurchasedItem>();
for (int i = 0; i < purchasedItemCount; i++)
{
string itemPrefabIdentifier = msg.ReadString();
UInt16 itemQuantity = msg.ReadUInt16();
purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity));
}
if (!sender.HasPermission(ClientPermissions.ManageCampaign))
{
DebugConsole.ThrowError("Client \"" + sender.Name + "\" does not have a permission to manage the campaign");
return;
}
if (purchasedHullRepairs != this.PurchasedHullRepairs)
{
if (purchasedHullRepairs && Money >= HullRepairCost)
{
this.PurchasedHullRepairs = true;
Money -= HullRepairCost;
}
else if (!purchasedHullRepairs)
{
this.PurchasedHullRepairs = false;
Money += HullRepairCost;
}
}
if (purchasedItemRepairs != this.PurchasedItemRepairs)
{
if (purchasedItemRepairs && Money >= ItemRepairCost)
{
this.PurchasedItemRepairs = true;
Money -= ItemRepairCost;
}
else if (!purchasedItemRepairs)
{
this.PurchasedItemRepairs = false;
Money += ItemRepairCost;
}
}
if (purchasedLostShuttles != this.PurchasedLostShuttles)
{
if (GameMain.GameSession?.Submarine != null &&
GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied)
{
GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox);
}
else if (purchasedLostShuttles && Money >= ShuttleReplaceCost)
{
this.PurchasedLostShuttles = true;
Money -= ShuttleReplaceCost;
}
else if (!purchasedItemRepairs)
{
this.PurchasedLostShuttles = false;
Money += ShuttleReplaceCost;
}
}
Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex);
if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); }
if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndex); }
List<PurchasedItem> currentItems = new List<PurchasedItem>(CargoManager.PurchasedItems);
foreach (PurchasedItem pi in currentItems)
{
CargoManager.SellItem(pi, pi.Quantity);
}
foreach (PurchasedItem pi in purchasedItems)
{
CargoManager.PurchaseItem(pi.ItemPrefab, pi.Quantity);
}
}
public override void Save(XElement element)
{
XElement modeElement = new XElement("MultiPlayerCampaign",
new XAttribute("money", Money),
new XAttribute("cheatsenabled", CheatsEnabled),
new XAttribute("initialsuppliesspawned", InitialSuppliesSpawned));
Map.Save(modeElement);
element.Add(modeElement);
//save character data to a separate file
string characterDataPath = GetCharacterDataSavePath();
XDocument characterDataDoc = new XDocument(new XElement("CharacterData"));
foreach (CharacterCampaignData cd in characterData)
{
characterDataDoc.Root.Add(cd.Save());
}
try
{
characterDataDoc.Save(characterDataPath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e);
}
lastSaveID++;
DebugConsole.Log("Campaign saved, save ID " + lastSaveID);
}
}
}
@@ -0,0 +1,36 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using System;
namespace Barotrauma.Items.Components
{
partial class Door
{
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen)
{
if (IsStuck || isOpen == open)
{
return;
}
isOpen = open;
//opening a partially stuck door makes it less stuck
if (isOpen) stuck = MathHelper.Clamp(stuck - 30.0f, 0.0f, 100.0f);
if (sendNetworkMessage)
{
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), forcedOpen });
}
}
public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
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);
msg.Write(lastUser == null ? (UInt16)0 : lastUser.ID);
}
}
}
@@ -0,0 +1,36 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
{
public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
base.ServerWrite(msg, c, extraData);
if (!attachable || body == null) { return; }
msg.Write(Attached);
msg.Write(body.SimPosition.X);
msg.Write(body.SimPosition.Y);
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
if (!item.CanClientAccess(c) || !Attachable || attached || !MathUtils.IsValid(simPosition)) { return; }
Vector2 offset = simPosition - c.Character.SimPosition;
offset = offset.ClampLength(MaxAttachDistance * 1.5f);
simPosition = c.Character.SimPosition + offset;
Drop(false, null);
item.SetTransform(simPosition, 0.0f);
AttachToWall();
item.CreateServerEvent(this);
GameServer.Log(c.Character.LogName + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction);
}
}
}
@@ -0,0 +1,14 @@
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class LevelResource : ItemComponent, IServerSerializable
{
private float lastSentDeattachTimer;
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(deattachTimer);
}
}
}
@@ -0,0 +1,22 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ItemComponent : ISerializableEntity
{
private bool LoadElemProjSpecific(XElement subElement)
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "guiframe":
break;
case "sound":
break;
default:
return false; //unknown element
}
return true; //element processed
}
}
}
@@ -0,0 +1,39 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ItemLabel : ItemComponent, IDrawableComponent
{
[Serialize("", true, description: "The text to display on the label."), Editable(100)]
public string Text
{
get;
set;
}
[Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label.")]
public Color TextColor
{
get;
set;
}
[Editable, Serialize(1.0f, true, description: "The scale of the text displayed on the label.")]
public float TextScale
{
get;
set;
}
public override void Move(Vector2 amount)
{
//do nothing
}
public ItemLabel(Item item, XElement element)
: base(item, element)
{
}
}
}
@@ -0,0 +1,13 @@
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class Controller : ItemComponent, IServerSerializable
{
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(state);
msg.Write(user == null ? (ushort)0 : user.ID);
}
}
}
@@ -0,0 +1,27 @@
using Barotrauma.Networking;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Deconstructor : Powered, IServerSerializable, IClientSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
bool active = msg.ReadBoolean();
item.CreateServerEvent(this);
if (item.CanClientAccess(c))
{
SetActive(active, c.Character);
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(IsActive);
msg.Write(progressTimer);
}
}
}
@@ -0,0 +1,32 @@
using Barotrauma.Networking;
using System;
namespace Barotrauma.Items.Components
{
partial class Engine : Powered, IServerSerializable, IClientSerializable
{
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
//force can only be adjusted at 10% intervals -> no need for more accuracy than this
msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10);
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
float newTargetForce = msg.ReadRangedInteger(-10, 10) * 10.0f;
if (item.CanClientAccess(c))
{
if (Math.Abs(newTargetForce - targetForce) > 0.01f)
{
GameServer.Log(c.Character.LogName + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction);
}
targetForce = newTargetForce;
}
//notify all clients of the changed state
item.CreateServerEvent(this);
}
}
}
@@ -0,0 +1,42 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Fabricator : Powered, IServerSerializable, IClientSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1);
item.CreateServerEvent(this);
if (!item.CanClientAccess(c)) return;
if (itemIndex == -1)
{
CancelFabricating(c.Character);
}
else
{
//if already fabricating the selected item, return
if (fabricatedItem != null && fabricationRecipes.IndexOf(fabricatedItem) == itemIndex) return;
if (itemIndex < 0 || itemIndex >= fabricationRecipes.Count) return;
StartFabricating(fabricationRecipes[itemIndex], c.Character);
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
int itemIndex = fabricatedItem == null ? -1 : fabricationRecipes.IndexOf(fabricatedItem);
msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1);
UInt16 userID = fabricatedItem == null || user == null ? (UInt16)0 : user.ID;
msg.Write(userID);
}
}
}
@@ -0,0 +1,42 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Pump : Powered, IServerSerializable, IClientSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
float newFlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f;
bool newIsActive = msg.ReadBoolean();
if (item.CanClientAccess(c))
{
if (newFlowPercentage != FlowPercentage)
{
GameServer.Log(c.Character.LogName + " set the pumping speed of " + item.Name + " to " + (int)(newFlowPercentage) + " %", ServerLog.MessageType.ItemInteraction);
}
if (newIsActive != IsActive)
{
GameServer.Log(c.Character.LogName + (newIsActive ? " turned on " : " turned off ") + item.Name, ServerLog.MessageType.ItemInteraction);
}
FlowPercentage = newFlowPercentage;
IsActive = newIsActive;
}
//notify all clients of the changed state
item.CreateServerEvent(this);
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
//flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this
msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10);
msg.Write(IsActive);
}
}
}
@@ -0,0 +1,54 @@
using Barotrauma.Networking;
using System;
namespace Barotrauma.Items.Components
{
partial class Reactor
{
private Client blameOnBroken;
private float? nextServerLogWriteTime;
private float lastServerLogWriteTime;
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
bool autoTemp = msg.ReadBoolean();
bool powerOn = msg.ReadBoolean();
float fissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8);
float turbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8);
if (!item.CanClientAccess(c)) return;
IsActive = true;
if (!autoTemp && AutoTemp) blameOnBroken = c;
if (turbineOutput < targetTurbineOutput) blameOnBroken = c;
if (fissionRate > targetFissionRate) blameOnBroken = c;
if (!_powerOn && powerOn) blameOnBroken = c;
AutoTemp = autoTemp;
_powerOn = powerOn;
targetFissionRate = fissionRate;
targetTurbineOutput = turbineOutput;
LastUser = c.Character;
if (nextServerLogWriteTime == null)
{
nextServerLogWriteTime = Math.Max(lastServerLogWriteTime + 1.0f, (float)Timing.TotalTime);
}
//need to create a server event to notify all clients of the changed state
unsentChanges = true;
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(autoTemp);
msg.Write(_powerOn);
msg.WriteRangedSingle(temperature, 0.0f, 100.0f, 8);
msg.WriteRangedSingle(targetFissionRate, 0.0f, 100.0f, 8);
msg.WriteRangedSingle(targetTurbineOutput, 0.0f, 100.0f, 8);
msg.WriteRangedSingle(degreeOfSuccess, 0.0f, 1.0f, 8);
}
}
}
@@ -0,0 +1,115 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
partial class Steering : Powered, IServerSerializable, IClientSerializable
{
// TODO: an enumeration would be much cleaner
public bool MaintainPos;
public bool LevelStartSelected;
public bool LevelEndSelected;
public bool UnsentChanges
{
get { return unsentChanges; }
set { unsentChanges = value; }
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c)
{
bool autoPilot = msg.ReadBoolean();
bool dockingButtonClicked = msg.ReadBoolean();
Vector2 newSteeringInput = targetVelocity;
bool maintainPos = false;
Vector2? newPosToMaintain = null;
bool headingToStart = false;
if (autoPilot)
{
maintainPos = msg.ReadBoolean();
if (maintainPos)
{
newPosToMaintain = new Vector2(
msg.ReadSingle(),
msg.ReadSingle());
}
else
{
headingToStart = msg.ReadBoolean();
}
}
else
{
newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle());
}
if (!item.CanClientAccess(c)) return;
user = c.Character;
AutoPilot = autoPilot;
if (dockingButtonClicked)
{
item.SendSignal(0, "1", "toggle_docking", sender: null);
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true });
}
if (!AutoPilot)
{
steeringInput = newSteeringInput;
steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel("helm") / 100.0f);
}
else
{
MaintainPos = newPosToMaintain != null;
posToMaintain = newPosToMaintain;
if (posToMaintain == null)
{
LevelStartSelected = headingToStart;
LevelEndSelected = !headingToStart;
UpdatePath();
}
else
{
LevelStartSelected = false;
LevelEndSelected = false;
}
}
//notify all clients of the changed state
unsentChanges = true;
}
public void ServerWrite(IWriteMessage msg, Barotrauma.Networking.Client c, object[] extraData = null)
{
msg.Write(autoPilot);
msg.Write(extraData.Length > 2 && extraData[2] is bool && (bool)extraData[2]);
if (!autoPilot)
{
//no need to write steering info if autopilot is controlling
msg.Write(steeringInput.X);
msg.Write(steeringInput.Y);
msg.Write(targetVelocity.X);
msg.Write(targetVelocity.Y);
msg.Write(steeringAdjustSpeed);
}
else
{
msg.Write(posToMaintain != null);
if (posToMaintain != null)
{
msg.Write(((Vector2)posToMaintain).X);
msg.Write(((Vector2)posToMaintain).Y);
}
else
{
msg.Write(LevelStartSelected);
}
}
}
}
}
@@ -0,0 +1,31 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class PowerContainer : Powered, IDrawableComponent, IServerSerializable, IClientSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
float newRechargeSpeed = msg.ReadRangedInteger(0, 10) / 10.0f * maxRechargeSpeed;
if (item.CanClientAccess(c))
{
RechargeSpeed = newRechargeSpeed;
GameServer.Log(c.Character.LogName + " set the recharge speed of " + item.Name + " to " + (int)((rechargeSpeed / maxRechargeSpeed) * 100.0f) + " %", ServerLog.MessageType.ItemInteraction);
}
item.CreateServerEvent(this);
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10);
float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f);
msg.WriteRangedSingle(chargeRatio, 0.0f, 1.0f, 8);
}
}
}
@@ -0,0 +1,45 @@
using Barotrauma.Networking;
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, IReadMessage msg, Client c)
{
if (c.Character == null) { return; }
var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2);
if (requestedFixAction != FixActions.None)
{
if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage)
{
if (GameSettings.VerboseLogging)
{
DebugConsole.Log($"Non traitor \"{c.Character.Name}\" attempted to sabotage item.");
}
requestedFixAction = FixActions.Repair;
}
if (CurrentFixer == null || CurrentFixer == c.Character && requestedFixAction != currentFixerAction)
{
StartRepairing(c.Character, requestedFixAction);
item.CreateServerEvent(this);
}
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(deteriorationTimer);
msg.Write(deteriorateAlwaysResetTimer);
msg.Write(DeteriorateAlways);
msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID);
msg.WriteRangedInteger((int)currentFixerAction, 0, 2);
}
}
}
@@ -0,0 +1,201 @@
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
List<Wire>[] wires = new List<Wire>[Connections.Count];
//read wire IDs for each connection
for (int i = 0; i < Connections.Count; i++)
{
wires[i] = new List<Wire>();
for (int j = 0; j < Connection.MaxLinked; j++)
{
ushort wireId = msg.ReadUInt16();
if (!(Entity.FindEntityByID(wireId) is Item wireItem)) { continue; }
Wire wireComponent = wireItem.GetComponent<Wire>();
if (wireComponent != null)
{
wires[i].Add(wireComponent);
}
}
}
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; }
item.CreateServerEvent(this);
//check if the character can access this connectionpanel
//and all the wires they're trying to connect
if (!item.CanClientAccess(c)) { return; }
for (int i = 0; i < Connections.Count; i++)
{
foreach (Wire wire in wires[i])
{
//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)) && !DisconnectedWires.Contains(wire))
{
if (!wire.Item.CanClientAccess(c)) { return; }
}
}
}
if (!CheckCharacterSuccess(c.Character))
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, c.Character.ID });
return;
}
//go through existing wire links
for (int i = 0; i < Connections.Count; i++)
{
int j = -1;
foreach (Wire existingWire in Connections[i].Wires)
{
j++;
if (existingWire == null) { continue; }
//existing wire not in the list of new wires -> disconnect it
if (!wires[i].Contains(existingWire))
{
if (existingWire.Locked)
{
//this should not be possible unless the client is running a modified version of the game
GameServer.Log(c.Character.LogName + " attempted to disconnect a locked wire from " +
Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Error);
continue;
}
existingWire.RemoveConnection(item);
if (existingWire.Item.ParentInventory == null)
{
item.GetComponent<ConnectionPanel>()?.DisconnectedWires.Add(existingWire);
}
if (!wires.Any(w => w.Contains(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.Wiring);
if (existingWire.Item.ParentInventory != null)
{
//in an inventory and not connected to anything -> the wire cannot have any nodes
existingWire.ClearConnections();
}
else if (!clientSideDisconnectedWires.Contains(existingWire))
{
//not in an inventory, not connected to anything, not hanging loose from any panel -> must be dropped
existingWire.Item.Drop(c.Character);
}
}
else if (existingWire.Connections[0] != null)
{
GameServer.Log(c.Character.LogName + " disconnected a wire from " +
Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + existingWire.Connections[0].Item.Name + " (" + existingWire.Connections[0].Name + ")", ServerLog.MessageType.Wiring);
//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)))
{
//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.Wiring);
/*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);
}
}
}
foreach (Wire disconnectedWire in DisconnectedWires.ToList())
{
if (disconnectedWire.Connections[0] == null &&
disconnectedWire.Connections[1] == null &&
!clientSideDisconnectedWires.Contains(disconnectedWire) &&
disconnectedWire.Item.ParentInventory == null)
{
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++)
{
foreach (Wire newWire in wires[i])
{
//already connected, no need to do anything
if (Connections[i].Wires.Contains(newWire)) { continue; }
Connections[i].TryAddLink(newWire);
newWire.Connect(Connections[i], true, true);
var otherConnection = newWire.OtherConnection(Connections[i]);
if (otherConnection == null)
{
GameServer.Log(c.Character.LogName + " connected a wire to " +
Connections[i].Item.Name + " (" + Connections[i].Name + ")",
ServerLog.MessageType.Wiring);
}
else
{
GameServer.Log(c.Character.LogName + " connected a wire from " +
Connections[i].Item.Name + " (" + Connections[i].Name + ") to " +
(otherConnection == null ? "none" : otherConnection.Item.Name + " (" + (otherConnection.Name) + ")"),
ServerLog.MessageType.Wiring);
}
}
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(user == null ? (ushort)0 : user.ID);
ClientWrite(msg, extraData);
}
}
}
@@ -0,0 +1,62 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
bool[] elementStates = new bool[customInterfaceElementList.Count];
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
elementStates[i] = msg.ReadBoolean();
}
CustomInterfaceElement clickedButton = null;
if (item.CanClientAccess(c))
{
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
if (customInterfaceElementList[i].ContinuousSignal)
{
TickBoxToggled(customInterfaceElementList[i], elementStates[i]);
}
else if (elementStates[i])
{
clickedButton = customInterfaceElementList[i];
ButtonClicked(customInterfaceElementList[i]);
}
}
}
//notify all clients of the new state
GameMain.Server.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
clickedButton
});
item.CreateServerEvent(this);
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
//extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked)
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
if (customInterfaceElementList[i].ContinuousSignal)
{
msg.Write(customInterfaceElementList[i].State);
}
else
{
msg.Write(extraData != null && extraData.Any(d => d as CustomInterfaceElement == customInterfaceElementList[i]));
}
}
}
}
}
@@ -0,0 +1,31 @@
using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class Terminal : ItemComponent, IClientSerializable, IServerSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
string newOutputValue = msg.ReadString();
if (item.CanClientAccess(c))
{
if (newOutputValue.Length > MaxMessageLength)
{
newOutputValue = newOutputValue.Substring(0, MaxMessageLength);
}
GameServer.Log(c.Character.LogName + " entered \"" + newOutputValue + "\" on " + item.Name,
ServerLog.MessageType.ItemInteraction);
OutputValue = newOutputValue;
item.SendSignal(0, newOutputValue, "signal_out", null);
item.CreateServerEvent(this);
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(OutputValue);
}
}
}
@@ -0,0 +1,64 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma.Items.Components
{
partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable
{
private void CreateNetworkEvent()
{
if (GameMain.Server == null) return;
//split into multiple events because one might not be enough to fit all the nodes
int eventCount = Math.Max((int)Math.Ceiling(nodes.Count / (float)MaxNodesPerNetworkEvent), 1);
for (int i = 0; i < eventCount; i++)
{
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i });
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
int eventIndex = (int)extraData[2];
int nodeStartIndex = eventIndex * MaxNodesPerNetworkEvent;
int nodeCount = MathHelper.Clamp(nodes.Count - nodeStartIndex, 0, MaxNodesPerNetworkEvent);
msg.WriteRangedInteger(eventIndex, 0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent));
msg.WriteRangedInteger(nodeCount, 0, MaxNodesPerNetworkEvent);
for (int i = nodeStartIndex; i < nodeStartIndex + nodeCount; i++)
{
msg.Write(nodes[i].X);
msg.Write(nodes[i].Y);
}
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
int nodeCount = msg.ReadByte();
Vector2 lastNodePos = Vector2.Zero;
if (nodeCount > 0)
{
lastNodePos = new Vector2(msg.ReadSingle(), msg.ReadSingle());
}
if (!item.CanClientAccess(c)) { return; }
if (nodes.Count > nodeCount)
{
nodes.RemoveRange(nodeCount, nodes.Count - nodeCount);
}
if (nodeCount > 0)
{
if (nodeCount > nodes.Count)
{
nodes.Add(lastNodePos);
}
else
{
nodes[nodes.Count - 1] = lastNodePos;
}
}
CreateNetworkEvent();
}
}
}
@@ -0,0 +1,149 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Inventory : IServerSerializable, IClientSerializable
{
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
List<Item> prevItems = new List<Item>(Items);
byte itemCount = msg.ReadByte();
ushort[] newItemIDs = new ushort[itemCount];
for (int i = 0; i < itemCount; i++)
{
newItemIDs[i] = msg.ReadUInt16();
}
if (c == null || c.Character == null) return;
bool accessible = c.Character.CanAccessInventory(this);
if (this is CharacterInventory && accessible)
{
if (Owner == null || !(Owner is Character))
{
accessible = false;
}
else if (!((CharacterInventory)this).AccessibleWhenAlive && !((Character)Owner).IsDead)
{
accessible = false;
}
}
if (!accessible)
{
//create a network event to correct the client's inventory state
//otherwise they may have an item in their inventory they shouldn't have been able to pick up,
//and receiving an event for that inventory later will cause the item to be dropped
CreateNetworkEvent();
for (int i = 0; i < capacity; i++)
{
if (!(Entity.FindEntityByID(newItemIDs[i]) is Item item)) { continue; }
item.PositionUpdateInterval = 0.0f;
if (item.ParentInventory != null && item.ParentInventory != this)
{
item.ParentInventory.CreateNetworkEvent();
}
}
return;
}
List<Inventory> prevItemInventories = new List<Inventory>(Items.Select(i => i?.ParentInventory));
for (int i = 0; i < capacity; i++)
{
Item newItem = newItemIDs[i] == 0 ? null : Entity.FindEntityByID(newItemIDs[i]) as Item;
prevItemInventories.Add(newItem?.ParentInventory);
if (newItemIDs[i] == 0 || (newItem != Items[i]))
{
if (Items[i] != null)
{
Item droppedItem = Items[i];
Entity prevOwner = Owner;
droppedItem.Drop(null);
if (droppedItem.body != null && prevOwner != null)
{
droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f);
}
}
System.Diagnostics.Debug.Assert(Items[i] == null);
}
}
for (int i = 0; i < capacity; i++)
{
if (newItemIDs[i] > 0)
{
if (!(Entity.FindEntityByID(newItemIDs[i]) is Item item) || item == Items[i]) { continue; }
if (GameMain.Server != null)
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
if (!prevItems.Contains(item) && !item.CanClientAccess(c))
{
item.PositionUpdateInterval = 0.0f;
continue;
}
}
TryPutItem(item, i, true, true, c.Character, false);
for (int j = 0; j < capacity; j++)
{
if (Items[j] == item && newItemIDs[j] != item.ID)
{
Items[j] = null;
}
}
}
}
CreateNetworkEvent();
foreach (Inventory prevInventory in prevItemInventories.Distinct())
{
if (prevInventory != this) prevInventory?.CreateNetworkEvent();
}
foreach (Item item in Items.Distinct())
{
if (item == null) { continue; }
if (!prevItems.Contains(item))
{
if (Owner == c.Character)
{
GameServer.Log(c.Character.LogName+ " picked up " + item.Name, ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log(c.Character.LogName + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory);
}
}
}
foreach (Item item in prevItems.Distinct())
{
if (item == null) { continue; }
if (!Items.Contains(item))
{
if (Owner == c.Character)
{
GameServer.Log(c.Character.LogName + " dropped " + item.Name, ServerLog.MessageType.Inventory);
}
else
{
GameServer.Log(c.Character.LogName + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory);
}
}
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
SharedWrite(msg, extraData);
}
}
}
@@ -0,0 +1,376 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable
{
public override Sprite Sprite
{
get { return prefab?.sprite; }
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
string errorMsg = "";
if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type))
{
if (extraData == null)
{
errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event data was null.";
}
else if (extraData.Length == 0)
{
errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event data was empty.";
}
else
{
errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event type not set.";
}
msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1);
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:InvalidData" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
int initialWritePos = msg.LengthBits;
NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0];
msg.WriteRangedInteger((int)eventType, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1);
switch (eventType)
{
case NetEntityEvent.Type.ComponentState:
if (extraData.Length < 2 || !(extraData[1] is int))
{
errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component index not given.";
break;
}
int componentIndex = (int)extraData[1];
if (componentIndex < 0 || componentIndex >= components.Count)
{
errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component index out of range (" + componentIndex + ").";
break;
}
else if (!(components[componentIndex] is IServerSerializable))
{
errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component \"" + components[componentIndex] + "\" is not server serializable.";
break;
}
msg.WriteRangedInteger(componentIndex, 0, components.Count - 1);
(components[componentIndex] as IServerSerializable).ServerWrite(msg, c, extraData);
break;
case NetEntityEvent.Type.InventoryState:
if (extraData.Length < 2 || !(extraData[1] is int))
{
errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component index not given.";
break;
}
int containerIndex = (int)extraData[1];
if (containerIndex < 0 || containerIndex >= components.Count)
{
errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - container index out of range (" + containerIndex + ").";
break;
}
else if (!(components[containerIndex] is ItemContainer))
{
errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component \"" + components[containerIndex] + "\" is not server serializable.";
break;
}
msg.WriteRangedInteger(containerIndex, 0, components.Count - 1);
msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
(components[containerIndex] as ItemContainer).Inventory.ServerWrite(msg, c);
break;
case NetEntityEvent.Type.Status:
msg.Write(condition);
break;
case NetEntityEvent.Type.Treatment:
{
ItemComponent targetComponent = (ItemComponent)extraData[1];
ActionType actionType = (ActionType)extraData[2];
ushort targetID = (ushort)extraData[3];
Limb targetLimb = (Limb)extraData[4];
Character targetCharacter = FindEntityByID(targetID) as Character;
byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255;
msg.Write((byte)components.IndexOf(targetComponent));
msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1);
msg.Write(targetID);
msg.Write(targetLimbIndex);
}
break;
case NetEntityEvent.Type.ApplyStatusEffect:
{
ActionType actionType = (ActionType)extraData[1];
ItemComponent targetComponent = extraData.Length > 2 ? (ItemComponent)extraData[2] : null;
ushort characterID = extraData.Length > 3 ? (ushort)extraData[3] : (ushort)0;
Limb targetLimb = extraData.Length > 4 ? (Limb)extraData[4] : null;
ushort useTargetID = extraData.Length > 5 ? (ushort)extraData[5] : (ushort)0;
Vector2? worldPosition = null;
if (extraData.Length > 6) { worldPosition = (Vector2)extraData[6]; }
Character targetCharacter = FindEntityByID(characterID) as Character;
byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255;
msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1);
msg.Write((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent)));
msg.Write(characterID);
msg.Write(targetLimbIndex);
msg.Write(useTargetID);
msg.Write(worldPosition.HasValue);
if (worldPosition.HasValue)
{
msg.Write(worldPosition.Value.X);
msg.Write(worldPosition.Value.Y);
}
}
break;
case NetEntityEvent.Type.ChangeProperty:
try
{
WritePropertyChange(msg, extraData, false);
}
catch (Exception e)
{
errorMsg = "Failed to write a ChangeProperty network event for the item \"" + Name + "\" (" + e.Message + ")";
}
break;
default:
errorMsg = "Failed to write a network event for the item \"" + Name + "\" - \"" + eventType + "\" is not a valid entity event type for items.";
break;
}
if (!string.IsNullOrEmpty(errorMsg))
{
//something went wrong - rewind the write position and write invalid event type to prevent creating an unreadable event
msg.BitPosition = initialWritePos;
msg.LengthBits = initialWritePos;
msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1);
DebugConsole.Log(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
}
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
NetEntityEvent.Type eventType =
(NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1);
c.KickAFKTimer = 0.0f;
switch (eventType)
{
case NetEntityEvent.Type.ComponentState:
int componentIndex = msg.ReadRangedInteger(0, components.Count - 1);
(components[componentIndex] as IClientSerializable).ServerRead(type, msg, c);
break;
case NetEntityEvent.Type.InventoryState:
int containerIndex = msg.ReadRangedInteger(0, components.Count - 1);
(components[containerIndex] as ItemContainer).Inventory.ServerRead(type, msg, c);
break;
case NetEntityEvent.Type.Treatment:
if (c.Character == null || !c.Character.CanInteractWith(this)) return;
UInt16 characterID = msg.ReadUInt16();
byte limbIndex = msg.ReadByte();
Character targetCharacter = FindEntityByID(characterID) as Character;
if (targetCharacter == null) break;
if (targetCharacter != c.Character && c.Character.SelectedCharacter != targetCharacter) break;
Limb targetLimb = limbIndex < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[limbIndex] : null;
if (ContainedItems == null || ContainedItems.All(i => i == null))
{
GameServer.Log(c.Character.LogName + " used item " + Name, ServerLog.MessageType.ItemInteraction);
}
else
{
GameServer.Log(
c.Character.LogName + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")",
ServerLog.MessageType.ItemInteraction);
}
ApplyTreatment(c.Character, targetCharacter, targetLimb);
break;
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, c.Character);
break;
}
}
public void WriteSpawnData(IWriteMessage msg, UInt16 entityID)
{
if (GameMain.Server == null) return;
msg.Write(Prefab.OriginalName);
msg.Write(Prefab.Identifier);
msg.Write(Description != prefab.Description);
if (Description != prefab.Description)
{
msg.Write(Description);
}
msg.Write(entityID);
if (ParentInventory == null || ParentInventory.Owner == null)
{
msg.Write((ushort)0);
msg.Write(Position.X);
msg.Write(Position.Y);
msg.Write(Submarine != null ? Submarine.ID : (ushort)0);
}
else
{
msg.Write(ParentInventory.Owner.ID);
//find the index of the ItemContainer this item is inside to get the item to
//spawn in the correct inventory in multi-inventory items like fabricators
byte containerIndex = 0;
if (Container != null)
{
for (int i = 0; i < Container.components.Count; i++)
{
if (Container.components[i] is ItemContainer container &&
container.Inventory == ParentInventory)
{
containerIndex = (byte)i;
break;
}
}
}
msg.Write(containerIndex);
int slotIndex = ParentInventory.FindIndex(this);
msg.Write(slotIndex < 0 ? (byte)255 : (byte)slotIndex);
}
byte teamID = 0;
foreach (WifiComponent wifiComponent in GetComponents<WifiComponent>())
{
teamID = (byte)wifiComponent.TeamID;
break;
}
msg.Write(teamID);
bool tagsChanged = tags.Count != prefab.Tags.Count || !tags.All(t => prefab.Tags.Contains(t));
msg.Write(tagsChanged);
if (tagsChanged)
{
msg.Write(Tags);
}
}
partial void UpdateNetPosition(float deltaTime)
{
if (parentInventory != null || body == null || !body.Enabled || Removed)
{
PositionUpdateInterval = float.PositiveInfinity;
return;
}
//gradually increase the interval of position updates
PositionUpdateInterval += deltaTime;
float maxInterval = 30.0f;
float velSqr = body.LinearVelocity.LengthSquared();
if (velSqr > 10.0f * 10.0f)
{
//over 10 m/s (projectile, thrown item or similar) -> send updates very frequently
maxInterval = 0.1f;
}
else if (velSqr > 1.0f)
{
//over 1 m/s
maxInterval = 0.25f;
}
else if (velSqr > 0.05f * 0.05f)
{
//over 0.05 m/s
maxInterval = 1.0f;
}
PositionUpdateInterval = Math.Min(PositionUpdateInterval, maxInterval);
}
public float GetPositionUpdateInterval(Client recipient)
{
if (PositionUpdateInterval == float.PositiveInfinity || body == null || parentInventory != null)
{
return float.PositiveInfinity;
}
if (recipient.Character == null || recipient.Character.IsDead)
{
//less frequent updates for clients who aren't controlling a character (max 2 updates/sec)
return Math.Max(PositionUpdateInterval, 0.5f);
}
else
{
float distSqr = Vector2.DistanceSquared(recipient.Character.WorldPosition, WorldPosition);
if (distSqr > 20000.0f * 20000.0f)
{
//don't send position updates at all if >20 000 units away
return float.PositiveInfinity;
}
else if (distSqr > 10000.0f * 10000.0f)
{
//drop the update rate to 10% if too far to see the item
return PositionUpdateInterval * 10;
}
else if (distSqr > 1000.0f * 1000.0f)
{
//halve the update rate if the client is far away (but still close enough to possibly see the item)
return PositionUpdateInterval * 2;
}
return PositionUpdateInterval;
}
}
public void ServerWritePosition(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(ID);
IWriteMessage tempBuffer = new WriteOnlyMessage();
body.ServerWrite(tempBuffer, c, extraData);
msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes);
msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
msg.WritePadBits();
}
public void CreateServerEvent<T>(T ic) where T : ItemComponent, IServerSerializable
{
if (GameMain.Server == null) return;
if (!ItemList.Contains(this))
{
string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
int index = components.IndexOf(ic);
if (index == -1) return;
GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ComponentState, index });
}
}
}
@@ -0,0 +1,134 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Hull : MapEntity, ISerializableEntity, IServerSerializable, IClientSerializable
{
private float lastSentVolume, lastSentOxygen, lastSentFireCount;
private float sendUpdateTimer;
public override bool IsMouseOn(Vector2 position)
{
return false;
}
partial void UpdateProjSpecific(float deltaTime, Camera cam)
{
if (IdFreed) { return; }
//don't create updates if all clients are very far from the hull
float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance;
if (!GameMain.Server.ConnectedClients.Any(c =>
c.Character != null &&
Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr))
{
return;
}
sendUpdateTimer -= deltaTime;
//update client hulls if the amount of water has changed by >10%
//or if oxygen percentage has changed by 5%
if (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f ||
Math.Abs(lastSentOxygen - OxygenPercentage) > 5f ||
lastSentFireCount != FireSources.Count ||
FireSources.Count > 0 ||
sendUpdateTimer < -NetConfig.SparseHullUpdateInterval)
{
if (sendUpdateTimer < 0.0f)
{
GameMain.NetworkMember.CreateEntityEvent(this);
lastSentVolume = waterVolume;
lastSentOxygen = OxygenPercentage;
lastSentFireCount = FireSources.Count;
sendUpdateTimer = NetConfig.HullUpdateInterval;
}
}
}
public void ServerWrite(IWriteMessage message, Client c, object[] extraData = null)
{
message.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
message.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8);
message.Write(FireSources.Count > 0);
if (FireSources.Count > 0)
{
message.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16);
for (int i = 0; i < Math.Min(FireSources.Count, 16); i++)
{
var fireSource = FireSources[i];
Vector2 normalizedPos = new Vector2(
(fireSource.Position.X - rect.X) / rect.Width,
(fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height);
message.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8);
message.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8);
message.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8);
}
}
}
//used when clients use the water/fire console commands
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
{
float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
bool hasFireSources = msg.ReadBoolean();
int fireSourceCount = 0;
List<Vector3> newFireSources = new List<Vector3>();
if (hasFireSources)
{
fireSourceCount = msg.ReadRangedInteger(0, 16);
for (int i = 0; i < fireSourceCount; i++)
{
newFireSources.Add(new Vector3(
MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f),
MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f),
msg.ReadRangedSingle(0.0f, 1.0f, 8)));
}
}
if (!c.HasPermission(ClientPermissions.ConsoleCommands) ||
!c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire")))
{
return;
}
WaterVolume = newWaterVolume;
for (int i = 0; i < fireSourceCount; i++)
{
Vector2 pos = new Vector2(
rect.X + rect.Width * newFireSources[i].X,
rect.Y - rect.Height + (rect.Height * newFireSources[i].Y));
float size = newFireSources[i].Z * rect.Width;
var newFire = i < FireSources.Count ?
FireSources[i] :
new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true);
newFire.Position = pos;
newFire.Size = new Vector2(size, newFire.Size.Y);
//ignore if the fire wasn't added to this room (invalid position)?
if (!FireSources.Contains(newFire))
{
newFire.Remove();
continue;
}
}
for (int i = FireSources.Count - 1; i >= fireSourceCount; i--)
{
FireSources[i].Remove();
if (i < FireSources.Count)
{
FireSources.RemoveAt(i);
}
}
}
}
}
@@ -0,0 +1,21 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
{
partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount)
{
GameMain.Server.KarmaManager.OnStructureHealthChanged(this, attacker, damageAmount);
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write((byte)Sections.Length);
for (int i = 0; i < Sections.Length; i++)
{
msg.WriteRangedSingle(Sections[i].damage / Health, 0.0f, 1.0f, 8);
}
}
}
}
@@ -0,0 +1,17 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class Submarine
{
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(ID);
IWriteMessage tempBuffer = new WriteOnlyMessage();
subBody.Body.ServerWrite(tempBuffer, c, extraData);
msg.Write((byte)tempBuffer.LengthBytes);
msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes);
msg.WritePadBits();
}
}
}
@@ -0,0 +1,370 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
namespace Barotrauma.Networking
{
partial class BannedPlayer
{
private static UInt16 LastIdentifier = 0;
public BannedPlayer(string name, string ip, string reason, DateTime? expirationTime)
{
this.Name = name;
this.IP = ip;
this.Reason = reason;
this.ExpirationTime = expirationTime;
this.UniqueIdentifier = LastIdentifier; LastIdentifier++;
this.IsRangeBan = IP.IndexOf(".x") > -1;
}
public BannedPlayer(string name, ulong steamID, string reason, DateTime? expirationTime)
{
this.Name = name;
this.SteamID = steamID;
this.Reason = reason;
this.ExpirationTime = expirationTime;
this.UniqueIdentifier = LastIdentifier; LastIdentifier++;
this.IsRangeBan = false;
this.IP = "";
}
public bool CompareTo(string ipCompare)
{
if (string.IsNullOrEmpty(IP) || string.IsNullOrEmpty(IP)) { return false; }
if (!IsRangeBan)
{
return ipCompare == IP;
}
else
{
int rangeBanIndex = IP.IndexOf(".x");
if (ipCompare.Length < rangeBanIndex) return false;
return ipCompare.Substring(0, rangeBanIndex) == IP.Substring(0, rangeBanIndex);
}
}
public bool CompareTo(IPAddress ipCompare)
{
if (string.IsNullOrEmpty(IP) || ipCompare == null) { return false; }
if (ipCompare.IsIPv4MappedToIPv6 && CompareTo(ipCompare.MapToIPv4NoThrow().ToString()))
{
return true;
}
return CompareTo(ipCompare.ToString());
}
}
partial class BanList
{
const string SavePath = "Data/bannedplayers.txt";
partial void InitProjectSpecific()
{
if (!File.Exists(SavePath)) { return; }
string[] lines;
try
{
lines = File.ReadAllLines(SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to open the list of banned players in " + SavePath, e);
return;
}
foreach (string line in lines)
{
string[] separatedLine = line.Split(',');
if (separatedLine.Length < 2) continue;
string name = separatedLine[0];
string identifier = separatedLine[1];
DateTime? expirationTime = null;
if (separatedLine.Length > 2 && !string.IsNullOrEmpty(separatedLine[2]))
{
if (DateTime.TryParse(separatedLine[2], out DateTime parsedTime))
{
expirationTime = parsedTime;
}
}
string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : "";
if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) continue;
if (identifier.Contains(".") || identifier.Contains(":"))
{
//identifier is an ip
bannedPlayers.Add(new BannedPlayer(name, identifier, reason, expirationTime));
}
else
{
//identifier should be a steam id
if (ulong.TryParse(identifier, out ulong steamID))
{
bannedPlayers.Add(new BannedPlayer(name, steamID, reason, expirationTime));
}
else
{
DebugConsole.ThrowError("Error in banlist: \"" + identifier + "\" is not a valid IP or a Steam ID");
}
}
}
}
public bool IsBanned(IPAddress IP, ulong steamID, out string reason)
{
reason = string.Empty;
if (IPAddress.IsLoopback(IP)) { return false; }
var bannedPlayer = bannedPlayers.Find(bp => bp.CompareTo(IP) || (steamID > 0 && bp.SteamID == steamID));
reason = bannedPlayer?.Reason;
return bannedPlayer != null;
}
public bool IsBanned(IPAddress IP, out string reason)
{
reason = string.Empty;
if (IPAddress.IsLoopback(IP)) { return false; }
bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value);
var bannedPlayer = bannedPlayers.Find(bp => bp.CompareTo(IP));
reason = bannedPlayer?.Reason;
return bannedPlayer != null;
}
public bool IsBanned(ulong steamID, out string reason)
{
reason = string.Empty;
bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value);
var bannedPlayer = bannedPlayers.Find(bp => steamID > 0 && bp.SteamID == steamID);
reason = bannedPlayer?.Reason;
return bannedPlayer != null;
}
public void BanPlayer(string name, IPAddress ip, string reason, TimeSpan? duration)
{
string ipStr = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4NoThrow().ToString() : ip.ToString();
BanPlayer(name, ipStr, 0, reason, duration);
}
public void BanPlayer(string name, string ip, string reason, TimeSpan? duration)
{
BanPlayer(name, ip, 0, reason, duration);
}
public void BanPlayer(string name, ulong steamID, string reason, TimeSpan? duration)
{
BanPlayer(name, "", steamID, reason, duration);
}
private void BanPlayer(string name, string ip, ulong steamID, string reason, TimeSpan? duration)
{
var existingBan = bannedPlayers.Find(bp => bp.IP == ip && bp.SteamID == steamID);
if (existingBan != null)
{
if (!duration.HasValue) return;
DebugConsole.Log("Set \"" + name + "\"'s ban duration to " + duration.Value);
existingBan.ExpirationTime = DateTime.Now + duration.Value;
Save();
return;
}
System.Diagnostics.Debug.Assert(!name.Contains(','));
string logMsg = "Banned " + name;
if (!string.IsNullOrEmpty(reason)) logMsg += ", reason: " + reason;
if (duration.HasValue) logMsg += ", duration: " + duration.Value.ToString();
DebugConsole.Log(logMsg);
DateTime? expirationTime = null;
if (duration.HasValue)
{
expirationTime = DateTime.Now + duration.Value;
}
if (!string.IsNullOrEmpty(ip))
{
bannedPlayers.Add(new BannedPlayer(name, ip, reason, expirationTime));
}
else if (steamID > 0)
{
bannedPlayers.Add(new BannedPlayer(name, steamID, reason, expirationTime));
}
else
{
DebugConsole.ThrowError("Failed to ban a client (no valid IP or Steam ID given)");
return;
}
Save();
}
public void UnbanPlayer(string name)
{
name = name.ToLower();
var player = bannedPlayers.Find(bp => bp.Name.ToLower() == name);
if (player == null)
{
DebugConsole.Log("Could not unban player \"" + name + "\". Matching player not found.");
}
else
{
RemoveBan(player);
}
}
public void UnbanIP(string ip)
{
var player = bannedPlayers.Find(bp => bp.IP == ip);
if (player == null)
{
DebugConsole.Log("Could not unban IP \"" + ip + "\". Matching player not found.");
}
else
{
RemoveBan(player);
}
}
private void RemoveBan(BannedPlayer banned)
{
DebugConsole.Log("Removing ban from " + banned.Name);
GameServer.Log("Removing ban from " + banned.Name, ServerLog.MessageType.ServerMessage);
bannedPlayers.Remove(banned);
Save();
}
private void RangeBan(BannedPlayer banned)
{
banned.IP = ToRange(banned.IP);
BannedPlayer bp;
while ((bp = bannedPlayers.Find(x => banned.CompareTo(x.IP))) != null)
{
//remove all specific bans that are now covered by the rangeban
bannedPlayers.Remove(bp);
}
bannedPlayers.Add(banned);
Save();
}
public void Save()
{
GameServer.Log("Saving banlist", ServerLog.MessageType.ServerMessage);
bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value);
List<string> lines = new List<string>();
foreach (BannedPlayer banned in bannedPlayers)
{
string line = banned.Name;
line += "," + ((banned.SteamID > 0) ? banned.SteamID.ToString() : banned.IP);
line += "," + (banned.ExpirationTime.HasValue ? banned.ExpirationTime.Value.ToString() : "");
if (!string.IsNullOrWhiteSpace(banned.Reason)) line += "," + banned.Reason;
lines.Add(line);
}
try
{
File.WriteAllLines(SavePath, lines);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving the list of banned players to " + SavePath + " failed", e);
}
}
public void ServerAdminWrite(IWriteMessage outMsg, Client c)
{
try
{
if (outMsg == null) { throw new ArgumentException("OutMsg was null"); }
if (GameMain.Server == null) { throw new Exception("GameMain.Server was null"); }
if (!c.HasPermission(ClientPermissions.Ban))
{
outMsg.Write(false); outMsg.WritePadBits();
return;
}
outMsg.Write(true);
outMsg.Write(c.Connection == GameMain.Server.OwnerConnection);
outMsg.WritePadBits();
outMsg.WriteVariableUInt32((UInt32)bannedPlayers.Count);
for (int i = 0; i < bannedPlayers.Count; i++)
{
BannedPlayer bannedPlayer = bannedPlayers[i];
outMsg.Write(bannedPlayer.Name);
outMsg.Write(bannedPlayer.UniqueIdentifier);
outMsg.Write(bannedPlayer.IsRangeBan); outMsg.WritePadBits();
if (c.Connection == GameMain.Server.OwnerConnection)
{
outMsg.Write(bannedPlayer.IP);
outMsg.Write(bannedPlayer.SteamID);
}
}
}
catch (Exception e)
{
string errorMsg = "Error while writing banlist. {" + e + "}\n" + e.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("Banlist.ServerAdminWrite", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
throw;
}
}
public bool ServerAdminRead(IReadMessage incMsg, Client c)
{
if (!c.HasPermission(ClientPermissions.Ban))
{
UInt16 removeCount = incMsg.ReadUInt16();
incMsg.BitPosition += removeCount * 4 * 8;
UInt16 rangeBanCount = incMsg.ReadUInt16();
incMsg.BitPosition += rangeBanCount * 4 * 8;
return false;
}
else
{
UInt16 removeCount = incMsg.ReadUInt16();
for (int i = 0; i < removeCount; i++)
{
UInt16 id = incMsg.ReadUInt16();
BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id);
if (bannedPlayer != null)
{
GameServer.Log(c.Name + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage);
RemoveBan(bannedPlayer);
}
}
Int16 rangeBanCount = incMsg.ReadInt16();
for (int i = 0; i < rangeBanCount; i++)
{
UInt16 id = incMsg.ReadUInt16();
BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id);
if (bannedPlayer != null)
{
GameServer.Log(c.Name + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage);
RangeBan(bannedPlayer);
}
}
return removeCount > 0 || rangeBanCount > 0;
}
}
}
}
@@ -0,0 +1,172 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Text;
namespace Barotrauma.Networking
{
partial class ChatMessage
{
public static void ServerRead(IReadMessage msg, Client c)
{
c.KickAFKTimer = 0.0f;
UInt16 ID = msg.ReadUInt16();
ChatMessageType type = (ChatMessageType)msg.ReadByte();
string txt = "";
int orderIndex = -1;
Character orderTargetCharacter = null;
Entity orderTargetEntity = null;
int orderOptionIndex = -1;
OrderChatMessage orderMsg = null;
if (type == ChatMessageType.Order)
{
orderIndex = msg.ReadByte();
orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character;
orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity;
orderOptionIndex = msg.ReadByte();
if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count)
{
DebugConsole.ThrowError("Invalid order message from client \"" + c.Name + "\" - order index out of bounds.");
return;
}
Order order = Order.PrefabList[orderIndex];
string orderOption = orderOptionIndex < 0 || orderOptionIndex >= order.Options.Length ? "" : order.Options[orderOptionIndex];
orderMsg = new OrderChatMessage(order, orderOption, orderTargetEntity, orderTargetCharacter, c.Character);
txt = orderMsg.Text;
}
else
{
txt = msg.ReadString() ?? "";
}
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) return;
c.LastSentChatMsgID = ID;
if (txt.Length > MaxLength)
{
txt = txt.Substring(0, MaxLength);
}
c.LastSentChatMessages.Add(txt);
if (c.LastSentChatMessages.Count > 10)
{
c.LastSentChatMessages.RemoveRange(0, c.LastSentChatMessages.Count - 10);
}
float similarity = 0.0f;
for (int i = 0; i < c.LastSentChatMessages.Count; i++)
{
float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i);
if (string.IsNullOrEmpty(txt))
{
similarity += closeFactor;
}
else
{
int levenshteinDist = ToolBox.LevenshteinDistance(txt, c.LastSentChatMessages[i]);
similarity += Math.Max((txt.Length - levenshteinDist) / (float)txt.Length * closeFactor, 0.0f);
}
}
//order/report messages can be sent a little faster than normal messages without triggering the spam filter
if (orderMsg != null)
{
similarity *= 0.25f;
}
bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection;
if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner)
{
GameMain.Server.KarmaManager.OnSpamFilterTriggered(c);
c.ChatSpamCount++;
if (c.ChatSpamCount > 3)
{
//kick for spamming too much
GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked"));
}
else
{
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
}
return;
}
c.ChatSpamSpeed += similarity + 0.5f;
if (c.ChatSpamTimer > 0.0f && !isOwner)
{
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
return;
}
if (type == ChatMessageType.Order)
{
if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) return;
ChatMessageType messageType = CanUseRadio(orderMsg.Sender) ? ChatMessageType.Radio : ChatMessageType.Default;
if (orderMsg.Order.TargetAllCharacters)
{
//do nothing
}
else if (orderTargetCharacter != null)
{
orderTargetCharacter.SetOrder(
new Order(orderMsg.Order.Prefab, orderTargetEntity, (orderTargetEntity as Item)?.Components.FirstOrDefault(ic => ic.GetType() == orderMsg.Order.ItemComponentType)),
orderMsg.OrderOption, orderMsg.Sender);
}
GameMain.Server.SendOrderChatMessage(orderMsg);
}
else
{
GameMain.Server.SendChatMessage(txt, null, c);
}
}
public int EstimateLengthBytesServer(Client c)
{
int length = 1 + //(byte)ServerNetObject.CHAT_MESSAGE
2 + //(UInt16)NetStateID
1 + //(byte)Type
Encoding.UTF8.GetBytes(Text).Length + 2;
if (Sender != null && c.InGame)
{
length += 2; //sender ID (UInt16)
}
else if (SenderName != null)
{
length += Encoding.UTF8.GetBytes(SenderName).Length + 2;
}
return length;
}
public virtual void ServerWrite(IWriteMessage msg, Client c)
{
msg.Write((byte)ServerNetObject.CHAT_MESSAGE);
msg.Write(NetStateID);
msg.Write((byte)Type);
msg.Write(Text);
msg.Write(SenderName);
msg.Write(Sender != null && c.InGame);
if (Sender != null && c.InGame)
{
msg.Write(Sender.ID);
}
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
namespace Barotrauma.Networking
{
static partial class ChildServerRelay
{
public static void Start(string writeHandle, string readHandle)
{
var writePipe = new AnonymousPipeClientStream(PipeDirection.Out, writeHandle);
var readPipe = new AnonymousPipeClientStream(PipeDirection.In, readHandle);
writeStream = writePipe; readStream = readPipe;
PrivateStart();
}
public static void ShutDown()
{
PrivateShutDown();
}
}
}
@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
partial class Client : IDisposable
{
public bool VoiceEnabled = true;
public UInt16 LastRecvClientListUpdate = 0;
public UInt16 LastRecvLobbyUpdate = 0;
public UInt16 LastSentChatMsgID = 0; //last msg this client said
public UInt16 LastRecvChatMsgID = 0; //last msg this client knows about
public UInt16 LastSentEntityEventID = 0;
public UInt16 LastRecvEntityEventID = 0;
public UInt16 LastRecvCampaignUpdate = 0;
public UInt16 LastRecvCampaignSave = 0;
public Pair<UInt16, float> LastCampaignSaveSendTime;
public readonly List<ChatMessage> ChatMsgQueue = new List<ChatMessage>();
public UInt16 LastChatMsgQueueID;
//latest chat messages sent by this client
public readonly List<string> LastSentChatMessages = new List<string>();
public float ChatSpamSpeed;
public float ChatSpamTimer;
public int ChatSpamCount;
public int RoundsSincePlayedAsTraitor;
public float KickAFKTimer;
public double MidRoundSyncTimeOut;
public bool NeedsMidRoundSync;
//how many unique events the client missed before joining the server
public UInt16 UnreceivedEntityEventCount;
public UInt16 FirstNewEventID;
//when was a specific entity event last sent to the client
// key = event id, value = NetTime.Now when sending
public readonly Dictionary<UInt16, double> EntityEventLastSent = new Dictionary<UInt16, double>();
//when was a position update for a given entity last sent to the client
// key = entity id, value = NetTime.Now when sending
public readonly Dictionary<UInt16, float> PositionUpdateLastSent = new Dictionary<UInt16, float>();
public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();
public bool ReadyToStart;
public List<Pair<JobPrefab, int>> JobPreferences;
public Pair<JobPrefab, int> AssignedJob;
public float DeleteDisconnectedTimer;
private CharacterInfo characterInfo;
public CharacterInfo CharacterInfo
{
get { return characterInfo; }
set
{
if (characterInfo == value) { return; }
characterInfo?.Remove();
characterInfo = value;
}
}
public NetworkConnection Connection { get; set; }
public bool SpectateOnly;
public int KarmaKickCount;
private float karma = 100.0f;
public float Karma
{
get
{
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 || !GameMain.Server.ServerSettings.KarmaEnabled) { return; }
karma = Math.Min(Math.Max(value, 0.0f), 100.0f);
}
}
partial void InitProjSpecific()
{
var jobs = JobPrefab.Prefabs.ToList();
// TODO: modding support?
JobPreferences = new List<Pair<JobPrefab, int>>(jobs.GetRange(0, Math.Min(jobs.Count, 3)).Select(j => new Pair<JobPrefab, int>(j, 0)));
VoipQueue = new VoipQueue(ID, true, true);
GameMain.Server.VoipServer.RegisterQueue(VoipQueue);
//initialize to infinity, gets set to a proper value when initializing midround syncing
MidRoundSyncTimeOut = double.PositiveInfinity;
}
partial void DisposeProjSpecific()
{
GameMain.Server.VoipServer.UnregisterQueue(VoipQueue);
VoipQueue.Dispose();
characterInfo?.Remove();
characterInfo = null;
}
public void InitClientSync()
{
LastSentChatMsgID = 0;
LastRecvChatMsgID = ChatMessage.LastID;
LastRecvLobbyUpdate = 0;
LastRecvEntityEventID = 0;
UnreceivedEntityEventCount = 0;
NeedsMidRoundSync = false;
}
public static bool IsValidName(string name, ServerSettings serverSettings)
{
char[] disallowedChars = new char[] { ';', ',', '<', '>', '/', '\\', '[', ']', '"', '?' };
if (name.Any(c => disallowedChars.Contains(c))) return false;
foreach (char character in name)
{
if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) return false;
}
return true;
}
public bool EndpointMatches(string endpoint)
{
if (Connection is LidgrenConnection lidgrenConn)
{
if (lidgrenConn.IPEndPoint?.Address == null) { return false; }
if ((lidgrenConn.IPEndPoint?.Address.IsIPv4MappedToIPv6 ?? false) &&
lidgrenConn.IPEndPoint?.Address.MapToIPv4NoThrow().ToString() == endpoint)
{
return true;
}
}
return Connection.EndPointString == endpoint;
}
public void SetPermissions(ClientPermissions permissions, List<DebugConsole.Command> permittedConsoleCommands)
{
this.Permissions = permissions;
this.PermittedConsoleCommands = new List<DebugConsole.Command>(permittedConsoleCommands);
}
public void GivePermission(ClientPermissions permission)
{
if (!this.Permissions.HasFlag(permission)) this.Permissions |= permission;
}
public void RemovePermission(ClientPermissions permission)
{
if (this.Permissions.HasFlag(permission)) this.Permissions &= ~permission;
}
public bool HasPermission(ClientPermissions permission)
{
return this.Permissions.HasFlag(permission);
}
}
}
@@ -0,0 +1,49 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class EntitySpawner : Entity, IServerSerializable
{
public void CreateNetworkEvent(Entity entity, bool remove)
{
CreateNetworkEventProjSpecific(entity, remove);
}
partial void CreateNetworkEventProjSpecific(Entity entity, bool remove)
{
if (GameMain.Server != null && entity != null)
{
GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) });
}
}
public void ServerWrite(IWriteMessage message, Client client, object[] extraData = null)
{
if (GameMain.Server == null) return;
SpawnOrRemove entities = (SpawnOrRemove)extraData[0];
message.Write(entities.Remove);
if (entities.Remove)
{
message.Write(entities.OriginalID);
}
else
{
if (entities.Entity is Item)
{
message.Write((byte)SpawnableType.Item);
DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")");
((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID);
}
else if (entities.Entity is Character)
{
message.Write((byte)SpawnableType.Character);
DebugConsole.Log("Writing character spawn data: " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")");
((Character)entities.Entity).WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true);
}
}
}
}
}
@@ -0,0 +1,357 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace Barotrauma.Networking
{
class FileSender
{
public class FileTransferOut
{
private readonly byte[] data;
private readonly DateTime startingTime;
private readonly NetworkConnection connection;
public FileTransferStatus Status;
public string FileName
{
get;
private set;
}
public string FilePath
{
get;
private set;
}
public FileTransferType FileType
{
get;
private set;
}
public float Progress
{
get { return KnownReceivedOffset / (float)Data.Length; }
}
public float WaitTimer
{
get;
set;
}
public byte[] Data
{
get { return data; }
}
public bool Acknowledged;
public int SentOffset
{
get;
set;
}
public int KnownReceivedOffset;
public NetworkConnection Connection
{
get { return connection; }
}
public int ID;
public FileTransferOut(NetworkConnection recipient, FileTransferType fileType, string filePath)
{
connection = recipient;
FileType = fileType;
FilePath = filePath;
FileName = Path.GetFileName(filePath);
Acknowledged = false;
SentOffset = 0;
KnownReceivedOffset = 0;
Status = FileTransferStatus.NotStarted;
startingTime = DateTime.Now;
int maxRetries = 4;
for (int i = 0; i <= maxRetries; i++)
{
try
{
data = File.ReadAllBytes(filePath);
}
catch (IOException e)
{
if (i >= maxRetries) { throw; }
DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red);
Thread.Sleep(250);
}
}
}
}
const int MaxTransferCount = 16;
const int MaxTransferCountPerRecipient = 5;
public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0);
public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver);
public FileTransferDelegate OnStarted;
public FileTransferDelegate OnEnded;
private readonly List<FileTransferOut> activeTransfers;
private readonly int chunkLen;
private readonly ServerPeer peer;
public List<FileTransferOut> ActiveTransfers
{
get { return activeTransfers; }
}
public FileSender(ServerPeer serverPeer, int mtu)
{
peer = serverPeer;
chunkLen = mtu - 100;
activeTransfers = new List<FileTransferOut>();
}
public FileTransferOut StartTransfer(NetworkConnection recipient, FileTransferType fileType, string filePath)
{
if (activeTransfers.Count >= MaxTransferCount)
{
return null;
}
if (activeTransfers.Count(t => t.Connection == recipient) > MaxTransferCountPerRecipient)
{
return null;
}
if (!File.Exists(filePath))
{
DebugConsole.ThrowError("Failed to initiate file transfer (file \"" + filePath + "\" not found.");
return null;
}
FileTransferOut transfer = null;
try
{
transfer = new FileTransferOut(recipient, fileType, filePath)
{
ID = 1
};
while (activeTransfers.Any(t => t.Connection == recipient && t.ID == transfer.ID))
{
transfer.ID++;
}
activeTransfers.Add(transfer);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to initiate file transfer", e);
return null;
}
OnStarted(transfer);
return transfer;
}
public void Update(float deltaTime)
{
activeTransfers.RemoveAll(t => t.Connection.Status != NetworkConnectionStatus.Connected);
var endedTransfers = activeTransfers.FindAll(t =>
t.Connection.Status != NetworkConnectionStatus.Connected ||
t.Status == FileTransferStatus.Finished ||
t.Status == FileTransferStatus.Canceled ||
t.Status == FileTransferStatus.Error);
foreach (FileTransferOut transfer in endedTransfers)
{
activeTransfers.Remove(transfer);
OnEnded(transfer);
}
foreach (FileTransferOut transfer in activeTransfers)
{
transfer.WaitTimer -= deltaTime;
if (transfer.WaitTimer > 0.0f) continue;
transfer.WaitTimer = 0.05f;// transfer.Connection.AverageRoundtripTime;
// send another part of the file
long remaining = transfer.Data.Length - transfer.SentOffset;
int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining);
IWriteMessage message;
try
{
//first message; send length, file name etc
//wait for acknowledgement before sending data
if (!transfer.Acknowledged)
{
message = new WriteOnlyMessage();
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
//if the recipient is the owner of the server (= a client running the server from the main exe)
//we don't need to send anything, the client can just read the file directly
if (transfer.Connection == GameMain.Server.OwnerConnection)
{
message.Write((byte)FileTransferMessageType.TransferOnSameMachine);
message.Write((byte)transfer.ID);
message.Write((byte)transfer.FileType);
message.Write(transfer.FilePath);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
transfer.Status = FileTransferStatus.Finished;
}
else
{
message.Write((byte)FileTransferMessageType.Initiate);
message.Write((byte)transfer.ID);
message.Write((byte)transfer.FileType);
//message.Write((ushort)chunkLen);
message.Write(transfer.Data.Length);
message.Write(transfer.FileName);
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
transfer.Status = FileTransferStatus.Sending;
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending file transfer initiation message: ");
DebugConsole.Log(" File: " + transfer.FileName);
DebugConsole.Log(" Size: " + transfer.Data.Length);
DebugConsole.Log(" ID: " + transfer.ID);
}
}
return;
}
message = new WriteOnlyMessage();
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
message.Write((byte)FileTransferMessageType.Data);
message.Write((byte)transfer.ID);
message.Write(transfer.SentOffset);
byte[] sendBytes = new byte[sendByteCount];
Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount);
message.Write((ushort)sendByteCount);
message.Write(sendBytes, 0, sendByteCount);
transfer.SentOffset += sendByteCount;
if (transfer.SentOffset > transfer.KnownReceivedOffset + chunkLen * 5 ||
transfer.SentOffset >= transfer.Data.Length)
{
transfer.SentOffset = transfer.KnownReceivedOffset;
}
peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
}
catch (Exception e)
{
DebugConsole.ThrowError("FileSender threw an exception when trying to send data", e);
GameAnalyticsManager.AddErrorEventOnce(
"FileSender.Update:Exception",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace);
transfer.Status = FileTransferStatus.Error;
break;
}
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending " + sendByteCount + " bytes of the file " + transfer.FileName + " (" + transfer.SentOffset + "/" + transfer.Data.Length + " sent)");
}
}
}
public void CancelTransfer(FileTransferOut transfer)
{
transfer.Status = FileTransferStatus.Canceled;
activeTransfers.Remove(transfer);
OnEnded(transfer);
GameMain.Server.SendCancelTransferMsg(transfer);
}
public void ReadFileRequest(IReadMessage inc, Client client)
{
byte messageType = inc.ReadByte();
if (messageType == (byte)FileTransferMessageType.Cancel)
{
byte transferId = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
if (matchingTransfer != null) CancelTransfer(matchingTransfer);
return;
}
else if (messageType == (byte)FileTransferMessageType.Data)
{
byte transferId = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
if (matchingTransfer != null)
{
matchingTransfer.Acknowledged = true;
int offset = inc.ReadInt32();
matchingTransfer.KnownReceivedOffset = offset > matchingTransfer.KnownReceivedOffset ? offset : matchingTransfer.KnownReceivedOffset;
if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset) { matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; }
if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length)
{
matchingTransfer.Status = FileTransferStatus.Finished;
}
}
}
byte fileType = inc.ReadByte();
switch (fileType)
{
case (byte)FileTransferType.Submarine:
string fileName = inc.ReadString();
string fileHash = inc.ReadString();
var requestedSubmarine = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash);
if (requestedSubmarine != null)
{
StartTransfer(inc.Sender, FileTransferType.Submarine, requestedSubmarine.FilePath);
}
break;
case (byte)FileTransferType.CampaignSave:
if (GameMain.GameSession != null &&
!ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave))
{
StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.SavePath);
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
{
client.LastCampaignSaveSendTime = new Pair<ushort, float>(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now);
}
}
break;
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,438 @@
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>>();
public float PreviousNotifiedKarma;
public float StructureDamageAccumulator;
private float structureDamagePerSecond;
public float StructureDamagePerSecond
{
get { return Math.Max(StructureDamageAccumulator, structureDamagePerSecond); }
set { structureDamagePerSecond = value; }
}
//when did a given character last attack this one
public Dictionary<Character, double> LastAttackTime
{
get;
private set;
} = new Dictionary<Character, double>();
}
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;
public void UpdateClients(IEnumerable<Client> clients, float deltaTime)
{
if (!GameMain.Server.GameStarted) { return; }
bannedClients.Clear();
foreach (Client client in clients)
{
UpdateClient(client, deltaTime);
if (perSecondUpdate < DateTime.Now)
{
var clientMemory = GetClientMemory(client);
clientMemory.StructureDamagePerSecond = clientMemory.StructureDamageAccumulator;
clientMemory.StructureDamageAccumulator = 0.0f;
var toRemove = clientMemory.LastAttackTime.Where(pair => pair.Value < Timing.TotalTime - AllowedRetaliationTime).Select(pair => pair.Key).ToList();
foreach (var lastAttacker in toRemove)
{
clientMemory.LastAttackTime.Remove(lastAttacker);
}
}
}
if (perSecondUpdate < DateTime.Now)
{
foreach (Client client in clients)
{
SendKarmaNotifications(client);
}
perSecondUpdate = DateTime.Now + new TimeSpan(0, 0, 1);
}
foreach (Client bannedClient in bannedClients)
{
bannedClient.KarmaKickCount++;
if (bannedClient.KarmaKickCount <= KicksBeforeBan)
{
GameMain.Server.KickClient(bannedClient, $"KarmaKicked~[banthreshold]={(int)KickBanThreshold}", resetKarma: true);
}
else
{
GameMain.Server.BanClient(bannedClient, $"KarmaBanned~[banthreshold]={(int)KickBanThreshold}", duration: TimeSpan.FromSeconds(GameMain.Server.ServerSettings.AutoBanTime));
}
}
}
private void SendKarmaNotifications(Client client, string debugKarmaChangeReason = "")
{
//send a notification about karma changing if the karma has changed by x%
var clientMemory = GetClientMemory(client);
float karmaChange = client.Karma - clientMemory.PreviousNotifiedKarma;
if (Math.Abs(karmaChange) > 1.0f &&
(TestMode || Math.Abs(karmaChange) / clientMemory.PreviousNotifiedKarma > KarmaNotificationInterval / 100.0f))
{
if (TestMode)
{
string msg =
karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your 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.Character != null && !client.Character.Removed && !client.Character.IsDead)
{
if (client.Karma > KarmaDecayThreshold)
{
client.Karma -= KarmaDecay * deltaTime;
}
else if (client.Karma < KarmaIncreaseThreshold)
{
client.Karma += KarmaIncrease * deltaTime;
}
//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;
if (herpesStrength <= 0.0f)
{
client.Character.CharacterHealth.ReduceAffliction(null, "invertcontrols", 100.0f);
}
}
//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 OnRoundEnded()
{
if (ResetKarmaBetweenRounds)
{
clientMemories.Clear();
foreach (Client client in GameMain.Server.ConnectedClients)
{
client.Karma = Math.Max(50.0f, client.Karma);
}
}
}
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?.Traitors != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == target))
{
//traitors always count as enemies
isEnemy = true;
}
if (GameMain.Server.TraitorManager.Traitors.Any(t =>
t.Character == attacker &&
t.CurrentObjective != null &&
t.CurrentObjective.IsEnemy(target)))
{
//target counts as an enemy to the traitor
isEnemy = true;
}
}
bool targetIsHusk = target.CharacterHealth?.GetAffliction<AfflictionHusk>("huskinfection")?.State == AfflictionHusk.InfectionState.Active;
bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction<AfflictionHusk>("huskinfection")?.State == AfflictionHusk.InfectionState.Active;
//huskified characters count as enemies to healthy characters and vice versa
if (targetIsHusk != attackerIsHusk) { 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;
}
}
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == target);
if (damage > 0 && targetClient != null)
{
var targetMemory = GetClientMemory(targetClient);
targetMemory.LastAttackTime[attacker] = Timing.TotalTime;
}
Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker);
if (attackerClient != null)
{
//if the attacker has been attacked by the target within the last x seconds, ignore the damage
//(= no karma penalty from retaliating against someone who attacked you)
var attackerMemory = GetClientMemory(attackerClient);
if (attackerMemory.LastAttackTime.ContainsKey(target) &&
attackerMemory.LastAttackTime[target] > Timing.TotalTime - AllowedRetaliationTime)
{
damage = Math.Min(damage, 0);
}
}
//attacking/healing clowns has a smaller effect on karma
if (target.HasEquippedItem("clownmask") &&
target.HasEquippedItem("clowncostume"))
{
damage *= 0.5f;
}
//smaller karma penalty for attacking someone who's aiming with a weapon
if (damage > 0.0f &&
target.IsKeyDown(InputType.Aim) &&
target.SelectedItems.Any(it => it != null && (it.GetComponent<MeleeWeapon>() != null || it.GetComponent<RangedWeapon>() != null)))
{
damage *= 0.5f;
}
//damage scales according to the karma of the target
//(= smaller karma penalty from attacking someone who has a low karma)
if (damage > 0 && targetClient != null)
{
damage *= MathUtils.InverseLerp(0.0f, 50.0f, targetClient.Karma);
}
if (isEnemy)
{
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; }
if (GameMain.Server.TraitorManager?.Traitors != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t =>
t.Character == attacker &&
t.CurrentObjective != null &&
t.CurrentObjective.IsAllowedToDamage(structure)))
{
//traitor tasked to flood the sub -> damaging structures is ok
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; }
//all penalties/rewards are halved when wearing a clown costume
if (target.HasEquippedItem("clownmask") &&
target.HasEquippedItem("clowncostume"))
{
amount *= 0.5f;
}
client.Karma += amount;
if (TestMode)
{
SendKarmaNotifications(client, debugKarmaChangeReason);
}
}
}
}
@@ -0,0 +1,567 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
class ServerEntityEvent : NetEntityEvent
{
private IServerSerializable serializable;
#if DEBUG
public string StackTrace;
#endif
private double createTime;
public double CreateTime
{
get { return createTime; }
}
public void ResetCreateTime()
{
createTime = Timing.TotalTime;
}
public ServerEntityEvent(IServerSerializable serializableEntity, UInt16 id)
: base(serializableEntity, id)
{
serializable = serializableEntity;
createTime = Timing.TotalTime;
#if DEBUG
StackTrace = Environment.StackTrace.ToString();
#endif
}
public void Write(IWriteMessage msg, Client recipient)
{
serializable.ServerWrite(msg, recipient, Data);
}
}
class ServerEntityEventManager : NetEntityEventManager
{
private List<ServerEntityEvent> events;
//list of unique events (i.e. !IsDuplicate) created during the round
//used for syncing clients who join mid-round
private List<ServerEntityEvent> uniqueEvents;
private UInt16 lastSentToAll;
private UInt16 lastSentToAnyone;
private double lastSentToAnyoneTime;
private double lastWarningTime;
public List<ServerEntityEvent> Events
{
get { return events; }
}
public List<ServerEntityEvent> UniqueEvents
{
get { return uniqueEvents; }
}
private class BufferedEvent
{
public readonly Client Sender;
public readonly UInt16 CharacterStateID;
public readonly ReadWriteMessage Data;
public readonly Character Character;
public readonly IClientSerializable TargetEntity;
public bool IsProcessed;
public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, ReadWriteMessage data)
{
this.Sender = sender;
this.Character = senderCharacter;
this.CharacterStateID = characterStateID;
this.TargetEntity = targetEntity;
this.Data = data;
}
}
private List<BufferedEvent> bufferedEvents;
private UInt16 ID;
private GameServer server;
private double lastEventCountHighWarning;
public ServerEntityEventManager(GameServer server)
{
events = new List<ServerEntityEvent>();
this.server = server;
bufferedEvents = new List<BufferedEvent>();
uniqueEvents = new List<ServerEntityEvent>();
lastWarningTime = -10.0;
}
public void CreateEvent(IServerSerializable entity, object[] extraData = null)
{
if (entity == null || !(entity is Entity))
{
DebugConsole.ThrowError("Can't create an entity event for " + entity + "!");
return;
}
if (((Entity)entity).Removed && !(entity is Level))
{
DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace);
return;
}
if (((Entity)entity).IdFreed)
{
DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace);
return;
}
var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
if (extraData != null) newEvent.SetData(extraData);
bool inGameClientsPresent = server.ConnectedClients.Count(c => c.InGame) > 0;
//remove old events that have been sent to all clients, they are redundant now
// keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
// and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced
events.RemoveAll(e => (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && e.CreateTime < Timing.TotalTime - 15.0f);
for (int i = events.Count - 1; i >= 0; i--)
{
//we already have an identical event that's waiting to be sent
// -> no need to add a new one
if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return;
}
ID++;
events.Add(newEvent);
if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
{
//create a copy of the event and give it a new ID
var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
uniqueEvent.SetData(extraData);
uniqueEvents.Add(uniqueEvent);
}
}
public void Update(List<Client> clients)
{
foreach (BufferedEvent bufferedEvent in bufferedEvents)
{
if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead)
{
bufferedEvent.IsProcessed = true;
continue;
}
//delay reading the events until the inputs for the corresponding frame have been processed
//UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored)
//atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored
if (!bufferedEvent.Character.IsUnconscious &&
NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
{
continue;
}
try
{
ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender);
}
catch (Exception e)
{
string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString();
if (GameSettings.VerboseLogging)
{
string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!";
GameServer.Log(errorMsg + "\n" + e.StackTrace, ServerLog.MessageType.Error);
DebugConsole.ThrowError(errorMsg, e);
}
GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace);
}
bufferedEvent.IsProcessed = true;
}
var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync);
if (inGameClients.Count > 0)
{
lastSentToAnyone = inGameClients[0].LastRecvEntityEventID;
lastSentToAll = inGameClients[0].LastRecvEntityEventID;
if (server.OwnerConnection != null)
{
var owner = clients.Find(c => c.Connection == server.OwnerConnection);
if (owner != null)
{
lastSentToAll = owner.LastRecvEntityEventID;
}
}
inGameClients.ForEach(c =>
{
if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) { lastSentToAll = c.LastRecvEntityEventID; }
if (NetIdUtils.IdMoreRecent(c.LastRecvEntityEventID, lastSentToAnyone)) { lastSentToAnyone = c.LastRecvEntityEventID; }
});
lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime;
if ((Timing.TotalTime - lastSentToAnyoneTime) > 10.0 && (Timing.TotalTime - lastWarningTime) > 5.0)
{
lastWarningTime = Timing.TotalTime;
GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage);
events.ForEach(e => e.ResetCreateTime());
//TODO: reset clients if this happens, maybe do it if a majority are behind rather than all of them?
}
clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); });
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
if (firstEventToResend != null && ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > 10.0 || (Timing.TotalTime - firstEventToResend.CreateTime) > 30.0))
{
// This event is 10 seconds older than the last one we've successfully sent,
// kick everyone that hasn't received it yet, this is way too old
// UNLESS the event was created when the client was still midround syncing,
// in which case we'll wait until the timeout runs out before kicking the client
List<Client> toKick = inGameClients.FindAll(c =>
NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event "
+ (c.LastRecvEntityEventID + 1).ToString() +
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
(lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" +
" Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncOldEvent + "/ServerMessage.ExcessiveDesyncOldEvent");
}
);
}
if (events.Count > 0)
{
//the client is waiting for an event that we don't have anymore
//(the ID they're expecting is smaller than the ID of the first event in our list)
List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent(events[0].ID, (UInt16)(c.LastRecvEntityEventID + 1)));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent");
});
}
}
var timedOutClients = clients.FindAll(c => c.Connection != GameMain.Server.OwnerConnection && c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
foreach (Client timedOutClient in timedOutClients)
{
GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error);
GameMain.Server.DisconnectClient(timedOutClient, "", DisconnectReason.SyncTimeout + "/ServerMessage.SyncTimeout");
}
bufferedEvents.RemoveAll(b => b.IsProcessed);
}
private void BufferEvent(BufferedEvent bufferedEvent)
{
if (bufferedEvents.Count > 512)
{
//should normally never happen
//a client could potentially spam events with a much higher character state ID
//than the state of their character and/or stop sending character inputs,
//so we'll drop some events to make sure no-one blows up our buffer
DebugConsole.Log("Excessive amount of events in a client's event buffer. The client may be spamming events or their event IDs might be out of sync. Dropping events...");
bufferedEvents.RemoveRange(0, 256);
}
bufferedEvents.Add(bufferedEvent);
}
public void RefreshEntityIDs()
{
events.ForEach(e => e.RefreshEntityID());
uniqueEvents.ForEach(e => e.RefreshEntityID());
}
/// <summary>
/// Writes all the events that the client hasn't received yet into the outgoing message
/// </summary>
public void Write(Client client, IWriteMessage msg)
{
Write(client, msg, out _);
}
/// <summary>
/// Writes all the events that the client hasn't received yet into the outgoing message
/// </summary>
public void Write(Client client, IWriteMessage msg, out List<NetEntityEvent> sentEvents)
{
List<NetEntityEvent> eventsToSync = null;
if (client.NeedsMidRoundSync)
{
eventsToSync = GetEventsToSync(client);
}
else
{
eventsToSync = GetEventsToSync(client);
}
if (eventsToSync.Count == 0)
{
sentEvents = eventsToSync;
return;
}
//too many events for one packet
//(normal right after a round has just started, don't show a warning if it's been less than 10 seconds)
if (eventsToSync.Count > 200 && GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 10.0)
{
if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0)
{
Color color = eventsToSync.Count > 500 ? Color.Red : Color.Orange;
if (eventsToSync.Count < 300) { color = Color.Yellow; }
string warningMsg = "WARNING: event count very high: " + eventsToSync.Count;
var sortedEvents = eventsToSync.GroupBy(e => e.Entity.ToString())
.Select(e => new { Value = e.Key, Count = e.Count() })
.OrderByDescending(e => e.Count);
int count = 1;
foreach (var sortedEvent in sortedEvents)
{
warningMsg += "\n" + count + ". " + (sortedEvent.Value?.ToString() ?? "null") + " x" + sortedEvent.Count;
count++;
if (count > 3) { break; }
}
if (GameSettings.VerboseLogging)
{
GameServer.Log(warningMsg, ServerLog.MessageType.Error);
}
DebugConsole.NewMessage(warningMsg, color);
lastEventCountHighWarning = Timing.TotalTime;
}
}
if (client.NeedsMidRoundSync)
{
msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL);
msg.Write(client.UnreceivedEntityEventCount);
msg.Write(client.FirstNewEventID);
Write(msg, eventsToSync, out sentEvents, client);
}
else
{
msg.Write((byte)ServerNetObject.ENTITY_EVENT);
Write(msg, eventsToSync, out sentEvents, client);
}
foreach (NetEntityEvent entityEvent in sentEvents)
{
(entityEvent as ServerEntityEvent).Sent = true;
client.EntityEventLastSent[entityEvent.ID] = Lidgren.Network.NetTime.Now;
}
}
/// <summary>
/// Returns a list of events that should be sent to the client from the eventList
/// </summary>
private List<NetEntityEvent> GetEventsToSync(Client client)
{
List<NetEntityEvent> eventsToSync = new List<NetEntityEvent>();
var eventList = client.NeedsMidRoundSync ? uniqueEvents : events;
if (eventList.Count == 0) { return eventsToSync; }
//find the index of the first event the client hasn't received
int startIndex = eventList.Count;
while (startIndex > 0 &&
NetIdUtils.IdMoreRecent(eventList[startIndex - 1].ID, client.LastRecvEntityEventID))
{
startIndex--;
}
for (int i = startIndex; i < eventList.Count; i++)
{
//find the first event that hasn't been sent in roundtriptime or at all
client.EntityEventLastSent.TryGetValue(eventList[i].ID, out double lastSent);
float avgRoundtripTime = 0.01f; //TODO: reimplement client.Connection.AverageRoundtripTime
float minInterval = Math.Max(avgRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2);
if (lastSent > Lidgren.Network.NetTime.Now - Math.Min(minInterval, 0.5f))
{
continue;
}
if (client.NeedsMidRoundSync)
{
if (i <= client.UnreceivedEntityEventCount)
{
eventsToSync.AddRange(eventList.GetRange(i, client.UnreceivedEntityEventCount - i));
}
}
else
{
eventsToSync.AddRange(eventList.GetRange(i, eventList.Count - i));
}
break;
}
return eventsToSync;
}
public void InitClientMidRoundSync(Client client)
{
//no need for midround syncing if no events have been created,
//or if the first created unique event is still in the event list
if (uniqueEvents.Count == 0 || (events.Count > 0 && events[0].ID == uniqueEvents[0].ID))
{
client.UnreceivedEntityEventCount = 0;
client.FirstNewEventID = 0;
client.NeedsMidRoundSync = false;
}
else
{
double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds;
midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f);
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
client.NeedsMidRoundSync = true;
client.MidRoundSyncTimeOut = Timing.TotalTime + midRoundSyncTimeOut;
//how many (unique) events the clients had missed before joining
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
//ID of the first event sent after the client joined
//(after the client has been synced they'll switch their lastReceivedID
//to the one before this, and the eventmanagers will start to function "normally")
client.FirstNewEventID = events.Count == 0 ? (UInt16)0 : events[events.Count - 1].ID;
}
}
/// <summary>
/// Read the events from the message, ignoring ones we've already received
/// </summary>
public void Read(IReadMessage msg, Client sender = null)
{
UInt16 firstEventID = msg.ReadUInt16();
int eventCount = msg.ReadByte();
for (int i = 0; i < eventCount; i++)
{
UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i);
UInt16 entityID = msg.ReadUInt16();
if (entityID == Entity.NullEntityID)
{
msg.ReadPadBits();
if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++;
continue;
}
byte msgLength = msg.ReadByte();
IClientSerializable entity = Entity.FindEntityByID(entityID) as IClientSerializable;
//skip the event if we've already received it
if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1))
{
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("Received msg " + thisEventID, Color.Red);
}
msg.BitPosition += msgLength * 8;
}
else if (entity == null)
{
//entity not found -> consider the even read and skip over it
//(can happen, for example, when a client uses a medical item repeatedly
//and creates an event for it before receiving the event about it being removed)
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage(
"Received msg " + thisEventID + ", entity " + entityID + " not found",
Microsoft.Xna.Framework.Color.Orange);
}
sender.LastSentEntityEventID++;
msg.BitPosition += msgLength * 8;
}
else
{
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("Received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green);
}
UInt16 characterStateID = msg.ReadUInt16();
ReadWriteMessage buffer = new ReadWriteMessage();
byte[] temp = msg.ReadBytes(msgLength - 2);
buffer.Write(temp, 0, msgLength - 2);
buffer.BitPosition = 0;
BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer));
sender.LastSentEntityEventID++;
}
msg.ReadPadBits();
}
}
protected override void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient = null)
{
var serverEvent = entityEvent as ServerEntityEvent;
if (serverEvent == null) return;
serverEvent.Write(buffer, recipient);
}
protected void ReadEvent(IReadMessage buffer, INetSerializable entity, Client sender = null)
{
var clientEntity = entity as IClientSerializable;
if (clientEntity == null) return;
clientEntity.ServerRead(ClientNetObject.ENTITY_STATE, buffer, sender);
}
public void Clear()
{
ID = 0;
events.Clear();
bufferedEvents.Clear();
lastSentToAll = 0;
uniqueEvents.Clear();
foreach (Client c in server.ConnectedClients)
{
c.EntityEventLastSent.Clear();
c.LastRecvEntityEventID = 0;
c.LastSentEntityEventID = 0;
}
}
}
}
@@ -0,0 +1,10 @@
namespace Barotrauma.Networking
{
abstract partial class NetworkMember
{
public Character Character
{
get { return null; }
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.Networking
{
partial class OrderChatMessage : ChatMessage
{
public override void ServerWrite(IWriteMessage msg, Client c)
{
msg.Write((byte)ServerNetObject.CHAT_MESSAGE);
msg.Write(NetStateID);
msg.Write((byte)ChatMessageType.Order);
msg.Write(SenderName);
msg.Write(Sender != null && c.InGame);
if (Sender != null && c.InGame)
{
msg.Write(Sender.ID);
}
msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab));
msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID);
msg.Write(TargetEntity == null ? (UInt16)0 : TargetEntity.ID);
msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption));
}
}
}
@@ -0,0 +1,694 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Linq;
using Lidgren.Network;
namespace Barotrauma.Networking
{
class LidgrenServerPeer : ServerPeer
{
private readonly ServerSettings serverSettings;
private NetPeerConfiguration netPeerConfiguration;
private NetServer netServer;
private class PendingClient
{
public string Name;
public int OwnerKey;
public NetConnection Connection;
public ConnectionInitialization InitializationStep;
public double UpdateTime;
public double TimeOut;
public int Retries;
public UInt64? SteamID;
public Int32? PasswordSalt;
public bool AuthSessionStarted;
public PendingClient(NetConnection conn)
{
OwnerKey = 0;
Connection = conn;
InitializationStep = ConnectionInitialization.SteamTicketAndVersion;
Retries = 0;
SteamID = null;
PasswordSalt = null;
UpdateTime = Timing.TotalTime + Timing.Step * 3.0;
TimeOut = NetworkConnection.TimeoutThreshold;
AuthSessionStarted = false;
}
}
private readonly List<LidgrenConnection> connectedClients;
private readonly List<PendingClient> pendingClients;
private readonly List<NetIncomingMessage> incomingLidgrenMessages;
public LidgrenServerPeer(int? ownKey, ServerSettings settings)
{
serverSettings = settings;
netServer = null;
connectedClients = new List<LidgrenConnection>();
pendingClients = new List<PendingClient>();
incomingLidgrenMessages = new List<NetIncomingMessage>();
ownerKey = ownKey;
}
public override void Start()
{
if (netServer != null) { return; }
netPeerConfiguration = new NetPeerConfiguration("barotrauma")
{
AcceptIncomingConnections = true,
AutoExpandMTU = false,
MaximumConnections = NetConfig.MaxPlayers * 2,
EnableUPnP = serverSettings.EnableUPnP,
Port = serverSettings.Port
};
netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage |
NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt |
NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error |
NetIncomingMessageType.UnconnectedData);
netPeerConfiguration.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
netServer = new NetServer(netPeerConfiguration);
netServer.Start();
if (serverSettings.EnableUPnP)
{
InitUPnP();
while (DiscoveringUPnP()) { }
FinishUPnP();
}
}
public override void Close(string msg = null)
{
if (netServer == null) { return; }
for (int i = pendingClients.Count - 1; i >= 0; i--)
{
RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg);
}
for (int i = connectedClients.Count - 1; i >= 0; i--)
{
Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString());
}
netServer.Shutdown(msg ?? DisconnectReason.ServerShutdown.ToString());
pendingClients.Clear();
connectedClients.Clear();
netServer = null;
Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange;
OnShutdown?.Invoke();
}
public override void Update(float deltaTime)
{
if (netServer == null) { return; }
if (OnOwnerDetermined != null && OwnerConnection != null)
{
OnOwnerDetermined?.Invoke(OwnerConnection);
OnOwnerDetermined = null;
}
netServer.ReadMessages(incomingLidgrenMessages);
//process incoming connections first
foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType == NetIncomingMessageType.ConnectionApproval))
{
HandleConnection(inc);
}
try
{
//after processing connections, go ahead with the rest of the messages
foreach (NetIncomingMessage inc in incomingLidgrenMessages.Where(m => m.MessageType != NetIncomingMessageType.ConnectionApproval))
{
switch (inc.MessageType)
{
case NetIncomingMessageType.Data:
HandleDataMessage(inc);
break;
case NetIncomingMessageType.StatusChanged:
HandleStatusChanged(inc);
break;
}
}
}
catch (Exception e)
{
string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("LidgrenServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
for (int i = 0; i < pendingClients.Count; i++)
{
PendingClient pendingClient = pendingClients[i];
UpdatePendingClient(pendingClient, deltaTime);
if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; }
}
incomingLidgrenMessages.Clear();
}
private void InitUPnP()
{
if (netServer == null) { return; }
netServer.UPnP.ForwardPort(netPeerConfiguration.Port, "barotrauma");
#if USE_STEAM
netServer.UPnP.ForwardPort(serverSettings.QueryPort, "barotrauma");
#endif
}
private bool DiscoveringUPnP()
{
if (netServer == null) { return false; }
return netServer.UPnP.Status == UPnPStatus.Discovering;
}
private void FinishUPnP()
{
//do nothing
}
private void HandleConnection(NetIncomingMessage inc)
{
if (netServer == null) { return; }
if (connectedClients.Count >= serverSettings.MaxPlayers)
{
inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString());
return;
}
if (serverSettings.BanList.IsBanned(inc.SenderConnection.RemoteEndPoint.Address, 0, out string banReason))
{
//IP banned: deny immediately
inc.SenderConnection.Deny(DisconnectReason.Banned.ToString() + "/ " + banReason);
return;
}
PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection);
if (pendingClient == null)
{
pendingClient = new PendingClient(inc.SenderConnection);
pendingClients.Add(pendingClient);
}
inc.SenderConnection.Approve();
}
private void HandleDataMessage(NetIncomingMessage inc)
{
if (netServer == null) { return; }
PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection);
byte incByte = inc.ReadByte();
bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0;
bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0;
if (isConnectionInitializationStep && pendingClient != null)
{
ReadConnectionInitializationStep(pendingClient, inc);
}
else if (!isConnectionInitializationStep)
{
LidgrenConnection conn = connectedClients.Find(c => c.NetConnection == inc.SenderConnection);
if (conn == null)
{
if (pendingClient != null)
{
RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired, "Received data message from unauthenticated client");
}
else if (inc.SenderConnection.Status != NetConnectionStatus.Disconnected &&
inc.SenderConnection.Status != NetConnectionStatus.Disconnecting)
{
inc.SenderConnection.Disconnect(DisconnectReason.AuthenticationRequired.ToString() + "/ Received data message from unauthenticated client");
}
return;
}
if (pendingClient != null) { pendingClients.Remove(pendingClient); }
if (serverSettings.BanList.IsBanned(conn.IPEndPoint.Address, conn.SteamID, out string banReason))
{
Disconnect(conn, DisconnectReason.Banned.ToString() + "/ " + banReason);
return;
}
UInt16 length = inc.ReadUInt16();
//DebugConsole.NewMessage(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte + " " + length);
IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, conn);
OnMessageReceived?.Invoke(conn, msg);
}
}
private void HandleStatusChanged(NetIncomingMessage inc)
{
if (netServer == null) { return; }
switch (inc.SenderConnection.Status)
{
case NetConnectionStatus.Disconnected:
string disconnectMsg;
LidgrenConnection conn = connectedClients.Find(c => c.NetConnection == inc.SenderConnection);
if (conn != null)
{
if (conn == OwnerConnection)
{
DebugConsole.NewMessage("Owner disconnected: closing the server...");
GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage);
Close(DisconnectReason.ServerShutdown.ToString() + "/ Owner disconnected");
}
else
{
disconnectMsg = $"ServerMessage.HasDisconnected~[client]={conn.Name}";
Disconnect(conn, disconnectMsg);
}
}
else
{
PendingClient pendingClient = pendingClients.Find(c => c.Connection == inc.SenderConnection);
if (pendingClient != null)
{
RemovePendingClient(pendingClient, DisconnectReason.Unknown, $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}");
}
}
break;
}
}
private void ReadConnectionInitializationStep(PendingClient pendingClient, NetIncomingMessage inc)
{
if (netServer == null) { return; }
pendingClient.TimeOut = NetworkConnection.TimeoutThreshold;
ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte();
//DebugConsole.NewMessage(initializationStep+" "+pendingClient.InitializationStep);
if (pendingClient.InitializationStep != initializationStep) return;
pendingClient.UpdateTime = Timing.TotalTime + Timing.Step;
switch (initializationStep)
{
case ConnectionInitialization.SteamTicketAndVersion:
string name = Client.SanitizeName(inc.ReadString());
int ownKey = inc.ReadInt32();
UInt64 steamId = inc.ReadUInt64();
UInt16 ticketLength = inc.ReadUInt16();
byte[] ticket = inc.ReadBytes(ticketLength);
if (!Client.IsValidName(name, serverSettings))
{
if (OwnerConnection != null ||
!IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4NoThrow()) &&
ownerKey == null || ownKey == 0 && ownKey != ownerKey)
{
RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid");
return;
}
}
string version = inc.ReadString();
bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false;
if (!isCompatibleVersion)
{
RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion,
$"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}");
GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red);
return;
}
int contentPackageCount = inc.ReadVariableInt32();
List<ClientContentPackage> clientContentPackages = new List<ClientContentPackage>();
for (int i = 0; i < contentPackageCount; i++)
{
string packageName = inc.ReadString();
string packageHash = inc.ReadString();
clientContentPackages.Add(new ClientContentPackage(packageName, packageHash));
}
//check if the client is missing any of our packages
List<ContentPackage> missingPackages = new List<ContentPackage>();
foreach (ContentPackage serverContentPackage in GameMain.SelectedPackages)
{
if (!serverContentPackage.HasMultiplayerIncompatibleContent) continue;
bool packageFound = clientContentPackages.Any(cp => cp.Name == serverContentPackage.Name && cp.Hash == serverContentPackage.MD5hash.Hash);
if (!packageFound) { missingPackages.Add(serverContentPackage); }
}
//check if the client is using packages we don't have
List<ClientContentPackage> redundantPackages = new List<ClientContentPackage>();
foreach (ClientContentPackage clientContentPackage in clientContentPackages)
{
bool packageFound = GameMain.SelectedPackages.Any(cp => cp.Name == clientContentPackage.Name && cp.MD5hash.Hash == clientContentPackage.Hash);
if (!packageFound) { redundantPackages.Add(clientContentPackage); }
}
if (missingPackages.Count == 1)
{
RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage,
$"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}");
GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error);
return;
}
else if (missingPackages.Count > 1)
{
List<string> packageStrs = new List<string>();
missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp)));
RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage,
$"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}");
GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error);
return;
}
if (redundantPackages.Count == 1)
{
RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage,
$"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr(redundantPackages[0])}");
GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (using an incompatible content package " + GetPackageStr(redundantPackages[0]) + ")", ServerLog.MessageType.Error);
return;
}
if (redundantPackages.Count > 1)
{
List<string> packageStrs = new List<string>();
redundantPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp)));
RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage,
$"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}");
GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (using incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error);
return;
}
if (pendingClient.SteamID == null)
{
bool requireSteamAuth = GameMain.Config.RequireSteamAuthentication;
#if DEBUG
requireSteamAuth = false;
#endif
//steam auth cannot be done (SteamManager not initialized or no ticket given),
//but it's not required either -> let the client join without auth
if ((!Steam.SteamManager.IsInitialized || (ticket?.Length??0) == 0) &&
!requireSteamAuth)
{
pendingClient.Name = name;
pendingClient.OwnerKey = ownKey;
pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder;
}
else
{
Steamworks.BeginAuthResult authSessionStartState = Steam.SteamManager.StartAuthSession(ticket, steamId);
if (authSessionStartState != Steamworks.BeginAuthResult.OK)
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: " + authSessionStartState.ToString());
return;
}
pendingClient.SteamID = steamId;
pendingClient.Name = name;
pendingClient.OwnerKey = ownKey;
pendingClient.AuthSessionStarted = true;
}
}
else //TODO: could remove since this seems impossible
{
if (pendingClient.SteamID != steamId)
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch");
return;
}
}
break;
case ConnectionInitialization.Password:
int pwLength = inc.ReadByte();
byte[] incPassword = new byte[pwLength];
inc.ReadBytes(incPassword, 0, pwLength);
if (pendingClient.PasswordSalt == null)
{
DebugConsole.ThrowError("Received password message from client without salt");
return;
}
if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value))
{
pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder;
}
else
{
pendingClient.Retries++;
if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan)
{
string banMsg = "Failed to enter correct password too many times";
if (pendingClient.SteamID != null)
{
serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID.Value, banMsg, null);
}
serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.Connection.RemoteEndPoint.Address, banMsg, null);
RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg);
return;
}
}
pendingClient.UpdateTime = Timing.TotalTime;
break;
case ConnectionInitialization.ContentPackageOrder:
pendingClient.InitializationStep = ConnectionInitialization.Success;
pendingClient.UpdateTime = Timing.TotalTime;
break;
}
}
private void UpdatePendingClient(PendingClient pendingClient, float deltaTime)
{
if (netServer == null) { return; }
if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, pendingClient.SteamID ?? 0, out string banReason))
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
return;
}
//DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep);
if (connectedClients.Count >= serverSettings.MaxPlayers)
{
RemovePendingClient(pendingClient, DisconnectReason.ServerFull, "");
}
if (pendingClient.InitializationStep == ConnectionInitialization.Success)
{
LidgrenConnection newConnection = new LidgrenConnection(pendingClient.Name, pendingClient.Connection, pendingClient.SteamID ?? 0)
{
Status = NetworkConnectionStatus.Connected
};
connectedClients.Add(newConnection);
pendingClients.Remove(pendingClient);
if (OwnerConnection == null &&
IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4NoThrow()) &&
ownerKey != null && pendingClient.OwnerKey != 0 && pendingClient.OwnerKey == ownerKey)
{
ownerKey = null;
OwnerConnection = newConnection;
}
OnInitializationComplete?.Invoke(newConnection);
return;
}
pendingClient.TimeOut -= deltaTime;
if (pendingClient.TimeOut < 0.0)
{
RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage);
}
if (Timing.TotalTime < pendingClient.UpdateTime) { return; }
pendingClient.UpdateTime = Timing.TotalTime + 1.0;
NetOutgoingMessage outMsg = netServer.CreateMessage();
outMsg.Write((byte)PacketHeader.IsConnectionInitializationStep);
outMsg.Write((byte)pendingClient.InitializationStep);
switch (pendingClient.InitializationStep)
{
case ConnectionInitialization.ContentPackageOrder:
var mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
outMsg.WriteVariableInt32(mpContentPackages.Count);
for (int i = 0; i < mpContentPackages.Count; i++)
{
outMsg.Write(mpContentPackages[i].MD5hash.Hash);
}
break;
case ConnectionInitialization.Password:
outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits();
if (pendingClient.PasswordSalt == null)
{
pendingClient.PasswordSalt = CryptoRandom.Instance.Next();
outMsg.Write(pendingClient.PasswordSalt.Value);
}
else
{
outMsg.Write(pendingClient.Retries);
}
break;
}
#if DEBUG
netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Server.SimulatedDuplicatesChance;
netPeerConfiguration.SimulatedMinimumLatency = GameMain.Server.SimulatedMinimumLatency;
netPeerConfiguration.SimulatedRandomLatency = GameMain.Server.SimulatedRandomLatency;
netPeerConfiguration.SimulatedLoss = GameMain.Server.SimulatedLoss;
#endif
NetSendResult result = netServer.SendMessage(outMsg, pendingClient.Connection, NetDeliveryMethod.ReliableUnordered);
if (result != NetSendResult.Sent && result != NetSendResult.Queued)
{
DebugConsole.NewMessage("Failed to send initialization step " + pendingClient.InitializationStep.ToString() + " to pending client: " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow);
}
//DebugConsole.NewMessage("sent update to pending client: " + pendingClient.InitializationStep);
}
private void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg)
{
if (netServer == null) { return; }
if (pendingClients.Contains(pendingClient))
{
pendingClients.Remove(pendingClient);
if (pendingClient.AuthSessionStarted)
{
Steam.SteamManager.StopAuthSession(pendingClient.SteamID.Value);
pendingClient.SteamID = null;
pendingClient.AuthSessionStarted = false;
}
pendingClient.Connection.Disconnect(reason + "/" + msg);
}
}
public override void InitializeSteamServerCallbacks()
{
Steamworks.SteamServer.OnValidateAuthTicketResponse += OnAuthChange;
}
private void OnAuthChange(Steamworks.SteamId steamID, Steamworks.SteamId ownerID, Steamworks.AuthResponse status)
{
if (netServer == null) { return; }
PendingClient pendingClient = pendingClients.Find(c => c.SteamID == steamID);
DebugConsole.Log(steamID + " validation: " + status+", "+(pendingClient!=null));
if (pendingClient == null)
{
if (status != Steamworks.AuthResponse.OK)
{
LidgrenConnection connection = connectedClients.Find(c => c.SteamID == steamID);
if (connection != null)
{
Disconnect(connection, DisconnectReason.SteamAuthenticationFailed.ToString() + "/ Steam authentication status changed: " + status.ToString());
}
}
return;
}
if (serverSettings.BanList.IsBanned(pendingClient.Connection.RemoteEndPoint.Address, steamID, out string banReason))
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
return;
}
if (status == Steamworks.AuthResponse.OK)
{
pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder;
pendingClient.UpdateTime = Timing.TotalTime;
}
else
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam authentication failed: " + status.ToString());
return;
}
}
public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod)
{
if (netServer == null) { return; }
if (!(conn is LidgrenConnection lidgrenConn)) return;
if (!connectedClients.Contains(lidgrenConn))
{
DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + lidgrenConn.IPString);
return;
}
NetDeliveryMethod lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable;
switch (deliveryMethod)
{
case DeliveryMethod.Unreliable:
lidgrenDeliveryMethod = NetDeliveryMethod.Unreliable;
break;
case DeliveryMethod.Reliable:
lidgrenDeliveryMethod = NetDeliveryMethod.ReliableUnordered;
break;
case DeliveryMethod.ReliableOrdered:
lidgrenDeliveryMethod = NetDeliveryMethod.ReliableOrdered;
break;
}
NetOutgoingMessage lidgrenMsg = netServer.CreateMessage();
byte[] msgData = new byte[msg.LengthBytes];
msg.PrepareForSending(ref msgData, out bool isCompressed, out int length);
lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None));
lidgrenMsg.Write((UInt16)length);
lidgrenMsg.Write(msgData, 0, length);
NetSendResult result = netServer.SendMessage(lidgrenMsg, lidgrenConn.NetConnection, lidgrenDeliveryMethod);
if (result != NetSendResult.Sent && result != NetSendResult.Queued)
{
DebugConsole.NewMessage("Failed to send message to "+conn.Name+": " + result.ToString(), Microsoft.Xna.Framework.Color.Yellow);
}
}
public override void Disconnect(NetworkConnection conn,string msg=null)
{
if (netServer == null) { return; }
if (!(conn is LidgrenConnection lidgrenConn)) { return; }
if (connectedClients.Contains(lidgrenConn))
{
lidgrenConn.Status = NetworkConnectionStatus.Disconnected;
connectedClients.Remove(lidgrenConn);
OnDisconnect?.Invoke(conn, msg);
Steam.SteamManager.StopAuthSession(conn.SteamID);
}
lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected");
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma.Networking
{
abstract class ServerPeer
{
protected struct ClientContentPackage
{
public string Name;
public string Hash;
public ClientContentPackage(string name, string hash)
{
Name = name; Hash = hash;
}
}
protected string GetPackageStr(ContentPackage contentPackage)
{
return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")";
}
protected string GetPackageStr(ClientContentPackage contentPackage)
{
return "\"" + contentPackage.Name + "\" (hash " + Md5Hash.GetShortHash(contentPackage.Hash) + ")";
}
public delegate void MessageCallback(NetworkConnection connection, IReadMessage message);
public delegate void DisconnectCallback(NetworkConnection connection, string reason);
public delegate void InitializationCompleteCallback(NetworkConnection connection);
public delegate void ShutdownCallback();
public delegate void OwnerDeterminedCallback(NetworkConnection connection);
public MessageCallback OnMessageReceived;
public DisconnectCallback OnDisconnect;
public InitializationCompleteCallback OnInitializationComplete;
public ShutdownCallback OnShutdown;
public OwnerDeterminedCallback OnOwnerDetermined;
protected int? ownerKey;
public NetworkConnection OwnerConnection { get; protected set; }
public abstract void InitializeSteamServerCallbacks();
public abstract void Start();
public abstract void Close(string msg = null);
public abstract void Update(float deltaTime);
public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod);
public abstract void Disconnect(NetworkConnection conn, string msg = null);
}
}
@@ -0,0 +1,578 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Linq;
using System.Threading;
namespace Barotrauma.Networking
{
class SteamP2PServerPeer : ServerPeer
{
private bool started;
private ServerSettings serverSettings;
public UInt64 OwnerSteamID
{
get;
private set;
}
private class PendingClient
{
public string Name;
public ConnectionInitialization InitializationStep;
public double UpdateTime;
public double TimeOut;
public int Retries;
public UInt64 SteamID;
public Int32? PasswordSalt;
public bool AuthSessionStarted;
public PendingClient(UInt64 steamId)
{
InitializationStep = ConnectionInitialization.SteamTicketAndVersion;
Retries = 0;
SteamID = steamId;
PasswordSalt = null;
UpdateTime = Timing.TotalTime+Timing.Step*3.0;
TimeOut = NetworkConnection.TimeoutThreshold;
AuthSessionStarted = false;
}
public void Heartbeat()
{
TimeOut = NetworkConnection.TimeoutThreshold;
}
}
private List<SteamP2PConnection> connectedClients;
private List<PendingClient> pendingClients;
public SteamP2PServerPeer(UInt64 steamId, ServerSettings settings)
{
serverSettings = settings;
connectedClients = new List<SteamP2PConnection>();
pendingClients = new List<PendingClient>();
ownerKey = null;
OwnerSteamID = steamId;
started = false;
}
public override void Start()
{
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.Write(OwnerSteamID);
outMsg.Write((byte)DeliveryMethod.Reliable);
outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage));
byte[] msgToSend = (byte[])outMsg.Buffer.Clone();
Array.Resize(ref msgToSend, outMsg.LengthBytes);
ChildServerRelay.Write(msgToSend);
started = true;
}
public override void Close(string msg = null)
{
if (!started) { return; }
if (OwnerConnection != null) OwnerConnection.Status = NetworkConnectionStatus.Disconnected;
for (int i = pendingClients.Count - 1; i >= 0; i--)
{
RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg);
}
for (int i = connectedClients.Count - 1; i >= 0; i--)
{
Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString());
}
pendingClients.Clear();
connectedClients.Clear();
ChildServerRelay.ShutDown();
OnShutdown?.Invoke();
}
public override void Update(float deltaTime)
{
if (!started) { return; }
if (OnOwnerDetermined != null && OwnerConnection != null)
{
OnOwnerDetermined?.Invoke(OwnerConnection);
OnOwnerDetermined = null;
}
//backwards for loop so we can remove elements while iterating
for (int i = connectedClients.Count - 1; i >= 0; i--)
{
connectedClients[i].Decay(deltaTime);
if (connectedClients[i].Timeout < 0.0)
{
Disconnect(connectedClients[i], "Timed out");
}
}
try
{
while (ChildServerRelay.Read(out byte[] incBuf))
{
IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, OwnerConnection);
HandleDataMessage(inc);
}
}
catch (Exception e)
{
string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("SteamP2PServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
for (int i = 0; i < pendingClients.Count; i++)
{
PendingClient pendingClient = pendingClients[i];
UpdatePendingClient(pendingClient);
if (i >= pendingClients.Count || pendingClients[i] != pendingClient) { i--; }
}
}
private void HandleDataMessage(IReadMessage inc)
{
if (!started) { return; }
UInt64 senderSteamId = inc.ReadUInt64();
byte incByte = inc.ReadByte();
bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0;
bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0;
bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0;
bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0;
bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0;
if (isServerMessage)
{
DebugConsole.ThrowError("Got server message from" + senderSteamId.ToString());
return;
}
if (senderSteamId != OwnerSteamID) //sender is remote, handle disconnects and heartbeats
{
PendingClient pendingClient = pendingClients.Find(c => c.SteamID == senderSteamId);
SteamP2PConnection connectedClient = connectedClients.Find(c => c.SteamID == senderSteamId);
pendingClient?.Heartbeat();
connectedClient?.Heartbeat();
if (serverSettings.BanList.IsBanned(senderSteamId, out string banReason))
{
if (pendingClient != null)
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
}
else if (connectedClient != null)
{
Disconnect(connectedClient, DisconnectReason.Banned.ToString() + "/ "+ banReason);
}
return;
}
else if (isDisconnectMessage)
{
if (pendingClient != null)
{
string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}";
RemovePendingClient(pendingClient, DisconnectReason.Unknown, disconnectMsg);
}
else if (connectedClient != null)
{
string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={connectedClient.Name}";
Disconnect(connectedClient, disconnectMsg, false);
}
return;
}
else if (isHeartbeatMessage)
{
//message exists solely as a heartbeat, ignore its contents
return;
}
else if (isConnectionInitializationStep)
{
if (pendingClient != null)
{
ReadConnectionInitializationStep(pendingClient, new ReadOnlyMessage(inc.Buffer, false, inc.BytePosition, inc.LengthBytes - inc.BytePosition, null));
}
else
{
ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte();
if (initializationStep == ConnectionInitialization.ConnectionStarted)
{
pendingClients.Add(new PendingClient(senderSteamId));
}
}
}
else if (connectedClient != null)
{
UInt16 length = inc.ReadUInt16();
IReadMessage msg = new ReadOnlyMessage(inc.Buffer, isCompressed, inc.BytePosition, length, connectedClient);
OnMessageReceived?.Invoke(connectedClient, msg);
}
}
else //sender is owner
{
if (OwnerConnection != null) { (OwnerConnection as SteamP2PConnection).Heartbeat(); }
if (isDisconnectMessage)
{
DebugConsole.ThrowError("Received disconnect message from owner");
return;
}
if (isServerMessage)
{
DebugConsole.ThrowError("Received server message from owner");
return;
}
if (isConnectionInitializationStep)
{
if (OwnerConnection == null)
{
string ownerName = inc.ReadString();
OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID)
{
Status = NetworkConnectionStatus.Connected
};
OnInitializationComplete?.Invoke(OwnerConnection);
}
return;
}
if (isHeartbeatMessage)
{
return;
}
else
{
UInt16 length = inc.ReadUInt16();
IReadMessage msg = new ReadOnlyMessage(inc.Buffer, isCompressed, inc.BytePosition, length, OwnerConnection);
OnMessageReceived?.Invoke(OwnerConnection, msg);
}
}
}
private void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc)
{
if (!started) { return; }
pendingClient.TimeOut = NetworkConnection.TimeoutThreshold;
ConnectionInitialization initializationStep = (ConnectionInitialization)inc.ReadByte();
//DebugConsole.NewMessage(initializationStep+" "+pendingClient.InitializationStep);
if (pendingClient.InitializationStep != initializationStep) return;
pendingClient.UpdateTime = Timing.TotalTime+Timing.Step;
switch (initializationStep)
{
case ConnectionInitialization.SteamTicketAndVersion:
string name = Client.SanitizeName(inc.ReadString());
UInt64 steamId = inc.ReadUInt64();
UInt16 ticketLength = inc.ReadUInt16();
inc.BitPosition += ticketLength * 8; //skip ticket, owner handles steam authentication
if (!Client.IsValidName(name, serverSettings))
{
RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid");
return;
}
string version = inc.ReadString();
bool isCompatibleVersion = NetworkMember.IsCompatible(version, GameMain.Version.ToString()) ?? false;
if (!isCompatibleVersion)
{
RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion,
$"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}");
GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red);
return;
}
int contentPackageCount = (int)inc.ReadVariableUInt32();
List<ClientContentPackage> clientContentPackages = new List<ClientContentPackage>();
for (int i = 0; i < contentPackageCount; i++)
{
string packageName = inc.ReadString();
string packageHash = inc.ReadString();
clientContentPackages.Add(new ClientContentPackage(packageName, packageHash));
}
//check if the client is missing any of our packages
List<ContentPackage> missingPackages = new List<ContentPackage>();
foreach (ContentPackage serverContentPackage in GameMain.SelectedPackages)
{
if (!serverContentPackage.HasMultiplayerIncompatibleContent) continue;
bool packageFound = clientContentPackages.Any(cp => cp.Name == serverContentPackage.Name && cp.Hash == serverContentPackage.MD5hash.Hash);
if (!packageFound) { missingPackages.Add(serverContentPackage); }
}
//check if the client is using packages we don't have
List<ClientContentPackage> redundantPackages = new List<ClientContentPackage>();
foreach (ClientContentPackage clientContentPackage in clientContentPackages)
{
bool packageFound = GameMain.SelectedPackages.Any(cp => cp.Name == clientContentPackage.Name && cp.MD5hash.Hash == clientContentPackage.Hash);
if (!packageFound) { redundantPackages.Add(clientContentPackage); }
}
if (missingPackages.Count == 1)
{
RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage,
$"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}");
GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error);
return;
}
else if (missingPackages.Count > 1)
{
List<string> packageStrs = new List<string>();
missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp)));
RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage,
$"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}");
GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error);
return;
}
if (redundantPackages.Count == 1)
{
RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage,
$"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr(redundantPackages[0])}");
GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (using an incompatible content package " + GetPackageStr(redundantPackages[0]) + ")", ServerLog.MessageType.Error);
return;
}
if (redundantPackages.Count > 1)
{
List<string> packageStrs = new List<string>();
redundantPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp)));
RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage,
$"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}");
GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (using incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error);
return;
}
if (!pendingClient.AuthSessionStarted)
{
pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder;
pendingClient.Name = name;
pendingClient.AuthSessionStarted = true;
}
break;
case ConnectionInitialization.Password:
int pwLength = inc.ReadByte();
byte[] incPassword = inc.ReadBytes(pwLength);
if (pendingClient.PasswordSalt == null)
{
DebugConsole.ThrowError("Received password message from client without salt");
return;
}
if (serverSettings.IsPasswordCorrect(incPassword, pendingClient.PasswordSalt.Value))
{
pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder;
}
else
{
pendingClient.Retries++;
if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan)
{
string banMsg = "Failed to enter correct password too many times";
serverSettings.BanList.BanPlayer(pendingClient.Name, pendingClient.SteamID, banMsg, null);
RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg);
return;
}
}
pendingClient.UpdateTime = Timing.TotalTime;
break;
case ConnectionInitialization.ContentPackageOrder:
pendingClient.InitializationStep = ConnectionInitialization.Success;
pendingClient.UpdateTime = Timing.TotalTime;
break;
}
}
private void UpdatePendingClient(PendingClient pendingClient)
{
if (!started) { return; }
if (serverSettings.BanList.IsBanned(pendingClient.SteamID, out string banReason))
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
return;
}
//DebugConsole.NewMessage("pending client status: " + pendingClient.InitializationStep);
if (connectedClients.Count >= serverSettings.MaxPlayers - 1)
{
RemovePendingClient(pendingClient, DisconnectReason.ServerFull, "");
}
if (pendingClient.InitializationStep == ConnectionInitialization.Success)
{
SteamP2PConnection newConnection = new SteamP2PConnection(pendingClient.Name, pendingClient.SteamID)
{
Status = NetworkConnectionStatus.Connected
};
connectedClients.Add(newConnection);
pendingClients.Remove(pendingClient);
OnInitializationComplete?.Invoke(newConnection);
}
pendingClient.TimeOut -= Timing.Step;
if (pendingClient.TimeOut < 0.0)
{
RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage);
}
if (Timing.TotalTime < pendingClient.UpdateTime) { return; }
pendingClient.UpdateTime = Timing.TotalTime + 1.0;
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.Write(pendingClient.SteamID);
outMsg.Write((byte)DeliveryMethod.Reliable);
outMsg.Write((byte)(PacketHeader.IsConnectionInitializationStep |
PacketHeader.IsServerMessage));
outMsg.Write((byte)pendingClient.InitializationStep);
switch (pendingClient.InitializationStep)
{
case ConnectionInitialization.ContentPackageOrder:
var mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count);
for (int i = 0; i < mpContentPackages.Count; i++)
{
outMsg.Write(mpContentPackages[i].MD5hash.Hash);
}
break;
case ConnectionInitialization.Password:
outMsg.Write(pendingClient.PasswordSalt == null); outMsg.WritePadBits();
if (pendingClient.PasswordSalt == null)
{
pendingClient.PasswordSalt = Lidgren.Network.CryptoRandom.Instance.Next();
outMsg.Write(pendingClient.PasswordSalt.Value);
}
else
{
outMsg.Write(pendingClient.Retries);
}
break;
}
byte[] msgToSend = (byte[])outMsg.Buffer.Clone();
Array.Resize(ref msgToSend, outMsg.LengthBytes);
ChildServerRelay.Write(msgToSend);
}
private void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string msg)
{
if (!started) { return; }
if (pendingClients.Contains(pendingClient))
{
SendDisconnectMessage(pendingClient.SteamID, reason + "/" + msg);
pendingClients.Remove(pendingClient);
if (pendingClient.AuthSessionStarted)
{
Steam.SteamManager.StopAuthSession(pendingClient.SteamID);
pendingClient.SteamID = 0;
pendingClient.AuthSessionStarted = false;
}
}
}
public override void InitializeSteamServerCallbacks()
{
throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!");
}
public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod)
{
if (!started) { return; }
if (!(conn is SteamP2PConnection steamp2pConn)) return;
if (!connectedClients.Contains(steamp2pConn) && conn != OwnerConnection)
{
DebugConsole.ThrowError("Tried to send message to unauthenticated connection: " + steamp2pConn.SteamID.ToString());
return;
}
IWriteMessage msgToSend = new WriteOnlyMessage();
byte[] msgData = new byte[msg.LengthBytes];
msg.PrepareForSending(ref msgData, out bool isCompressed, out int length);
msgToSend.Write(conn.SteamID);
msgToSend.Write((byte)deliveryMethod);
msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage));
msgToSend.Write((UInt16)length);
msgToSend.Write(msgData, 0, length);
byte[] bufToSend = (byte[])msgToSend.Buffer.Clone();
Array.Resize(ref bufToSend, msgToSend.LengthBytes);
ChildServerRelay.Write(bufToSend);
}
private void SendDisconnectMessage(UInt64 steamId, string msg)
{
if (!started) { return; }
if (string.IsNullOrWhiteSpace(msg)) { return; }
IWriteMessage msgToSend = new WriteOnlyMessage();
msgToSend.Write(steamId);
msgToSend.Write((byte)DeliveryMethod.Reliable);
msgToSend.Write((byte)(PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage));
msgToSend.Write(msg);
byte[] bufToSend = (byte[])msgToSend.Buffer.Clone();
Array.Resize(ref bufToSend, msgToSend.LengthBytes);
ChildServerRelay.Write(bufToSend);
}
private void Disconnect(NetworkConnection conn, string msg, bool sendDisconnectMessage)
{
if (!started) { return; }
if (!(conn is SteamP2PConnection steamp2pConn)) { return; }
if (connectedClients.Contains(steamp2pConn))
{
if (sendDisconnectMessage) SendDisconnectMessage(steamp2pConn.SteamID, msg);
steamp2pConn.Status = NetworkConnectionStatus.Disconnected;
connectedClients.Remove(steamp2pConn);
OnDisconnect?.Invoke(conn, msg);
Steam.SteamManager.StopAuthSession(conn.SteamID);
}
else if (steamp2pConn == OwnerConnection)
{
//TODO: fix?
}
}
public override void Disconnect(NetworkConnection conn, string msg = null)
{
Disconnect(conn, msg, true);
}
}
}
@@ -0,0 +1,356 @@
using Barotrauma.Items.Components;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
partial class RespawnManager : Entity, IServerSerializable
{
private List<Client> GetClientsToRespawn()
{
return networkMember.ConnectedClients.FindAll(c =>
c.InGame &&
(!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection)) &&
(c.Character == null || c.Character.IsDead));
}
private List<CharacterInfo> GetBotsToRespawn()
{
if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal)
{
return Character.CharacterList
.FindAll(c => c.TeamID == Character.TeamType.Team1 && c.AIController != null && c.Info != null && c.IsDead)
.Select(c => c.Info)
.ToList();
}
int currPlayerCount = GameMain.Server.ConnectedClients.Count(c =>
c.InGame &&
(!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection)));
var existingBots = Character.CharacterList
.FindAll(c => c.TeamID == Character.TeamType.Team1 && c.AIController != null && c.Info != null);
int requiredBots = GameMain.Server.ServerSettings.BotCount - currPlayerCount;
requiredBots -= existingBots.Count(b => !b.IsDead);
List<CharacterInfo> botsToRespawn = new List<CharacterInfo>();
for (int i = 0; i < requiredBots; i++)
{
CharacterInfo botToRespawn = existingBots.Find(b => b.IsDead)?.Info;
if (botToRespawn == null)
{
botToRespawn = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
}
else
{
existingBots.Remove(botToRespawn.Character);
}
botsToRespawn.Add(botToRespawn);
}
return botsToRespawn;
}
private bool RespawnPending()
{
int characterToRespawnCount = GetClientsToRespawn().Count;
int totalCharacterCount = GameMain.Server.ConnectedClients.Count;
return (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f);
}
partial void UpdateWaiting(float deltaTime)
{
bool respawnPending = RespawnPending();
if (respawnPending != RespawnCountdownStarted)
{
RespawnCountdownStarted = respawnPending;
RespawnTime = DateTime.Now + new TimeSpan(0,0,0,0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f));
GameMain.Server.CreateEntityEvent(this);
}
if (!RespawnCountdownStarted) { return; }
if (DateTime.Now > RespawnTime)
{
DispatchShuttle();
RespawnCountdownStarted = false;
}
if (RespawnShuttle == null) { return; }
RespawnShuttle.Velocity = Vector2.Zero;
if (shuttleSteering != null)
{
shuttleSteering.AutoPilot = false;
shuttleSteering.MaintainPos = false;
}
}
partial void DispatchShuttle()
{
if (RespawnShuttle != null)
{
CurrentState = State.Transporting;
GameMain.Server.CreateEntityEvent(this);
ResetShuttle();
if (shuttleSteering != null)
{
shuttleSteering.TargetVelocity = Vector2.Zero;
}
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
RespawnCharacters();
CoroutineManager.StopCoroutines("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
{
CurrentState = State.Waiting;
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
RespawnCharacters();
}
}
partial void UpdateReturningProjSpecific()
{
foreach (Door door in shuttleDoors)
{
if (door.IsOpen) door.TrySetState(false, false, true);
}
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);
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))
{
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(
ForceShuttleToPos(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + 1000.0f), 100.0f), "forcepos");
}
}
if (RespawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || DateTime.Now > despawnTime)
{
CoroutineManager.StopCoroutines("forcepos");
ResetShuttle();
CurrentState = State.Waiting;
GameServer.Log("The respawn shuttle has left.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
RespawnCountdownStarted = false;
}
}
partial void UpdateTransportingProjSpecific(float deltaTime)
{
if (!ReturnCountdownStarted)
{
//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 (DateTime.Now > ReturnTime)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
CurrentState = State.Returning;
GameMain.Server.CreateEntityEvent(this);
RespawnCountdownStarted = false;
maxTransportTime = GameMain.Server.ServerSettings.MaxTransportTime;
}
}
partial void RespawnCharactersProjSpecific()
{
var respawnSub = RespawnShuttle ?? Submarine.MainSub;
var clients = GetClientsToRespawn();
foreach (Client c in clients)
{
//get rid of the existing character
c.Character?.DespawnNow();
//all characters are in Team 1 in game modes/missions with only one team.
//if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked
c.TeamID = Character.TeamType.Team1;
if (c.CharacterInfo == null) { c.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); }
}
List<CharacterInfo> characterInfos = clients.Select(c => c.CharacterInfo).ToList();
var botsToSpawn = GetBotsToRespawn();
characterInfos.AddRange(botsToSpawn);
GameMain.Server.AssignJobs(clients);
foreach (Client c in clients)
{
c.CharacterInfo.Job = new Job(c.AssignedJob.First, c.AssignedJob.Second);
}
//the spawnpoints where the characters will spawn
var shuttleSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, respawnSub);
//the spawnpoints where they would spawn if they were spawned inside the main sub
//(in order to give them appropriate ID card tags)
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub);
ItemPrefab divingSuitPrefab = MapEntityPrefab.Find(null, "divingsuit") as ItemPrefab;
ItemPrefab oxyPrefab = MapEntityPrefab.Find(null, "oxygentank") as ItemPrefab;
ItemPrefab scooterPrefab = MapEntityPrefab.Find(null, "underwaterscooter") as ItemPrefab;
ItemPrefab batteryPrefab = MapEntityPrefab.Find(null, "batterycell") as ItemPrefab;
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
for (int i = 0; i < characterInfos.Count; i++)
{
bool bot = i >= clients.Count;
characterInfos[i].CurrentOrder = null;
characterInfos[i].CurrentOrderOption = null;
var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, !bot, bot);
character.TeamID = Character.TeamType.Team1;
if (bot)
{
GameServer.Log(string.Format("Respawning bot {0} as {1}", character.Info.Name, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
}
else
{
//tell the respawning client they're no longer a traitor
if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null)
{
if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == clients[i].Character))
{
GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("TraitorRespawnMessage"), clients[i], ChatMessageType.ServerMessageBox);
}
}
clients[i].Character = character;
character.OwnerClientEndPoint = clients[i].Connection.EndPointString;
character.OwnerClientName = clients[i].Name;
GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
}
if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null)
{
Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position;
if (divingSuitPrefab != null && oxyPrefab != null)
{
var divingSuit = new Item(divingSuitPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(divingSuit, false);
respawnItems.Add(divingSuit);
var oxyTank = new Item(oxyPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(oxyTank, false);
divingSuit.Combine(oxyTank, user: null);
respawnItems.Add(oxyTank);
}
if (scooterPrefab != null && batteryPrefab != null)
{
var scooter = new Item(scooterPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(scooter, false);
var battery = new Item(batteryPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(battery, false);
scooter.Combine(battery, user: null);
respawnItems.Add(scooter);
respawnItems.Add(battery);
}
}
//give the character the items they would've gotten if they had spawned in the main sub
character.GiveJobItems(mainSubSpawnPoints[i]);
//add the ID card tags they should've gotten when spawning in the shuttle
foreach (Item item in character.Inventory.Items)
{
if (item == null || item.Prefab.Identifier != "idcard") continue;
foreach (string s in shuttleSpawnPoints[i].IdCardTags)
{
item.AddTag(s);
}
if (!string.IsNullOrWhiteSpace(shuttleSpawnPoints[i].IdCardDesc))
item.Description = shuttleSpawnPoints[i].IdCardDesc;
}
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.WriteRangedInteger((int)CurrentState, 0, Enum.GetNames(typeof(State)).Length);
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();
}
}
}
@@ -0,0 +1,539 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
namespace Barotrauma.Networking
{
partial class ServerSettings
{
public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml";
partial void InitProjSpecific()
{
LoadSettings();
LoadClientPermissions();
}
private void WriteNetProperties(IWriteMessage outMsg)
{
outMsg.Write((UInt16)netProperties.Keys.Count);
foreach (UInt32 key in netProperties.Keys)
{
outMsg.Write(key);
netProperties[key].Write(outMsg);
}
}
public void ServerAdminWrite(IWriteMessage outMsg, Client c)
{
//outMsg.Write(isPublic);
//outMsg.Write(EnableUPnP);
//outMsg.WritePadBits();
//outMsg.Write((UInt16)QueryPort);
WriteNetProperties(outMsg);
WriteMonsterEnabled(outMsg);
BanList.ServerAdminWrite(outMsg, c);
Whitelist.ServerAdminWrite(outMsg, c);
}
public void ServerWrite(IWriteMessage outMsg, Client c)
{
outMsg.Write(ServerName);
outMsg.Write(ServerMessageText);
outMsg.Write((byte)MaxPlayers);
outMsg.Write(HasPassword);
outMsg.Write(IsPublic);
outMsg.WritePadBits();
outMsg.WriteRangedInteger(TickRate, 1, 60);
WriteExtraCargo(outMsg);
Voting.ServerWrite(outMsg);
if (c.HasPermission(Networking.ClientPermissions.ManageSettings))
{
outMsg.Write(true);
outMsg.WritePadBits();
ServerAdminWrite(outMsg, c);
}
else
{
outMsg.Write(false);
outMsg.WritePadBits();
}
}
public void ServerRead(IReadMessage incMsg, Client c)
{
if (!c.HasPermission(Networking.ClientPermissions.ManageSettings)) return;
NetFlags flags = (NetFlags)incMsg.ReadByte();
bool changed = false;
if (flags.HasFlag(NetFlags.Name))
{
string serverName = incMsg.ReadString();
if (ServerName != serverName) changed = true;
ServerName = serverName;
}
if (flags.HasFlag(NetFlags.Message))
{
string serverMessageText = incMsg.ReadString();
if (ServerMessageText != serverMessageText) changed = true;
ServerMessageText = serverMessageText;
}
if (flags.HasFlag(NetFlags.Properties))
{
changed |= ReadExtraCargo(incMsg);
UInt32 count = incMsg.ReadUInt32();
for (int i = 0; i < count; i++)
{
UInt32 key = incMsg.ReadUInt32();
if (netProperties.ContainsKey(key))
{
object prevValue = netProperties[key].Value;
netProperties[key].Read(incMsg);
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
{
UInt32 size = incMsg.ReadVariableUInt32();
incMsg.BitPosition += (int)(8 * size);
}
}
bool changedMonsterSettings = incMsg.ReadBoolean(); incMsg.ReadPadBits();
changed |= changedMonsterSettings;
if (changedMonsterSettings) ReadMonsterEnabled(incMsg);
changed |= BanList.ServerAdminRead(incMsg, c);
changed |= Whitelist.ServerAdminRead(incMsg, c);
}
if (flags.HasFlag(NetFlags.Misc))
{
int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
GameMain.NetLobbyScreen.MissionType = (Barotrauma.MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits);
int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1;
if (traitorSetting < 0) traitorSetting = 2;
if (traitorSetting > 2) traitorSetting = 0;
TraitorsEnabled = (YesNoMaybe)traitorSetting;
int botCount = BotCount + incMsg.ReadByte() - 1;
if (botCount < 0) botCount = MaxBotCount;
if (botCount > MaxBotCount) botCount = 0;
BotCount = botCount;
int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1;
if (botSpawnMode < 0) botSpawnMode = 1;
if (botSpawnMode > 1) botSpawnMode = 0;
BotSpawnMode = (BotSpawnMode)botSpawnMode;
float levelDifficulty = incMsg.ReadSingle();
if (levelDifficulty >= 0.0f) SelectedLevelDifficulty = levelDifficulty;
UseRespawnShuttle = incMsg.ReadBoolean();
bool changedAutoRestart = incMsg.ReadBoolean();
bool autoRestart = incMsg.ReadBoolean();
if (changedAutoRestart)
{
AutoRestart = autoRestart;
}
changed |= true;
}
if (flags.HasFlag(NetFlags.LevelSeed))
{
GameMain.NetLobbyScreen.LevelSeed = incMsg.ReadString();
changed |= true;
}
if (changed)
{
if (KarmaPreset == "custom")
{
GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset();
GameMain.NetworkMember?.KarmaManager?.Save();
}
SaveSettings();
GameMain.NetLobbyScreen.LastUpdateID++;
}
}
public void SaveSettings()
{
XDocument doc = new XDocument(new XElement("serversettings"));
doc.Root.SetAttributeValue("name", ServerName);
doc.Root.SetAttributeValue("public", IsPublic);
doc.Root.SetAttributeValue("port", Port);
#if USE_STEAM
doc.Root.SetAttributeValue("queryport", QueryPort);
#endif
doc.Root.SetAttributeValue("password", password ?? "");
doc.Root.SetAttributeValue("enableupnp", EnableUPnP);
doc.Root.SetAttributeValue("autorestart", autoRestart);
doc.Root.SetAttributeValue("LevelDifficulty", ((int)selectedLevelDifficulty).ToString());
doc.Root.SetAttributeValue("ServerMessage", ServerMessageText);
doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes));
doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => c.First + "-" + c.Second)));
SerializableProperty.SerializeProperties(this, doc.Root, true);
XmlWriterSettings settings = new XmlWriterSettings
{
Indent = true,
NewLineOnAttributes = true
};
using (var writer = XmlWriter.Create(SettingsFile, settings))
{
doc.Save(writer);
}
if (KarmaPreset == "custom")
{
GameMain.Server?.KarmaManager?.SaveCustomPreset();
}
GameMain.Server?.KarmaManager?.Save();
}
private void LoadSettings()
{
XDocument doc = null;
if (File.Exists(SettingsFile))
{
doc = XMLExtensions.TryLoadXml(SettingsFile);
}
if (doc == null)
{
doc = new XDocument(new XElement("serversettings"));
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, doc.Root);
AutoRestart = doc.Root.GetAttributeBool("autorestart", false);
Voting.AllowSubVoting = SubSelectionMode == SelectionMode.Vote;
Voting.AllowModeVoting = ModeSelectionMode == SelectionMode.Vote;
selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f);
GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty);
GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled);
string[] defaultAllowedClientNameChars =
new string[] {
"32-33",
"38-46",
"48-57",
"65-90",
"91",
"93",
"95-122",
"192-255",
"384-591",
"1024-1279",
"19968-40959","13312-19903","131072-15043983","15043985-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK
};
string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", defaultAllowedClientNameChars);
if (doc.Root.GetAttributeString("AllowedClientNameChars", "") == "65-90,97-122,48-59")
{
allowedClientNameCharsStr = defaultAllowedClientNameChars;
}
foreach (string allowedClientNameCharRange in allowedClientNameCharsStr)
{
string[] splitRange = allowedClientNameCharRange.Split('-');
if (splitRange.Length == 0 || splitRange.Length > 2)
{
DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names.");
continue;
}
int min = -1;
if (!int.TryParse(splitRange[0], out min))
{
DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names.");
continue;
}
int max = min;
if (splitRange.Length == 2)
{
if (!int.TryParse(splitRange[1], out max))
{
DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names.");
continue;
}
}
if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Pair<int, int>(min, max)); }
}
AllowedRandomMissionTypes = new List<MissionType>();
string[] allowedMissionTypeNames = doc.Root.GetAttributeStringArray(
"AllowedRandomMissionTypes", Enum.GetValues(typeof(MissionType)).Cast<MissionType>().Select(m => m.ToString()).ToArray());
foreach (string missionTypeName in allowedMissionTypeNames)
{
if (Enum.TryParse(missionTypeName, out MissionType missionType))
{
if (missionType == Barotrauma.MissionType.None) continue;
AllowedRandomMissionTypes.Add(missionType);
}
}
ServerName = doc.Root.GetAttributeString("name", "");
if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); }
ServerMessageText = doc.Root.GetAttributeString("ServerMessage", "");
GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier;
//handle Random as the mission type, which is no longer a valid setting
//MissionType.All offers equivalent functionality
if (MissionType == "Random") { MissionType = "All"; }
GameMain.NetLobbyScreen.MissionTypeName = MissionType;
GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode);
GameMain.NetLobbyScreen.SetBotCount(BotCount);
List<string> monsterNames = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList();
MonsterEnabled = new Dictionary<string, bool>();
foreach (string s in monsterNames)
{
if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true);
}
}
public void LoadClientPermissions()
{
ClientPermissions.Clear();
if (!File.Exists(ClientPermissionsFile))
{
if (File.Exists("Data/clientpermissions.txt"))
{
LoadClientPermissionsOld("Data/clientpermissions.txt");
}
return;
}
XDocument doc = XMLExtensions.TryLoadXml(ClientPermissionsFile);
if (doc == null) { return; }
foreach (XElement clientElement in doc.Root.Elements())
{
string clientName = clientElement.GetAttributeString("name", "");
string clientEndPoint = clientElement.GetAttributeString("endpoint", null) ?? clientElement.GetAttributeString("ip", "");
string steamIdStr = clientElement.GetAttributeString("steamid", "");
if (string.IsNullOrWhiteSpace(clientName))
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name and an IP address.");
continue;
}
if (string.IsNullOrWhiteSpace(clientEndPoint) && string.IsNullOrWhiteSpace(steamIdStr))
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have an IP address or a Steam ID.");
continue;
}
ClientPermissions permissions = Networking.ClientPermissions.None;
List<DebugConsole.Command> permittedCommands = new List<DebugConsole.Command>();
if (clientElement.Attribute("preset") == null)
{
string permissionsStr = clientElement.GetAttributeString("permissions", "");
if (permissionsStr.Equals("all", StringComparison.OrdinalIgnoreCase))
{
foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
{
permissions |= permission;
}
}
else if (!Enum.TryParse(permissionsStr, out permissions))
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + permissionsStr + "\" is not a valid client permission.");
continue;
}
if (permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands))
{
foreach (XElement commandElement in clientElement.Elements())
{
if (!commandElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; }
string commandName = commandElement.GetAttributeString("name", "");
DebugConsole.Command command = DebugConsole.FindCommand(commandName);
if (command == null)
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + commandName + "\" is not a valid console command.");
continue;
}
permittedCommands.Add(command);
}
}
}
else
{
string presetName = clientElement.GetAttributeString("preset", "");
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName);
if (preset == null)
{
DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found.");
return;
}
else
{
permissions = preset.Permissions;
permittedCommands = preset.PermittedCommands.ToList();
}
}
if (!string.IsNullOrEmpty(steamIdStr))
{
if (ulong.TryParse(steamIdStr, out ulong steamID))
{
ClientPermissions.Add(new SavedClientPermission(clientName, steamID, permissions, permittedCommands));
}
else
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + steamIdStr + "\" is not a valid Steam ID.");
continue;
}
}
else
{
ClientPermissions.Add(new SavedClientPermission(clientName, clientEndPoint, permissions, permittedCommands));
}
}
}
/// <summary>
/// Method for loading old .txt client permission files to provide backwards compatibility
/// </summary>
private void LoadClientPermissionsOld(string file)
{
if (!File.Exists(file)) return;
string[] lines;
try
{
lines = File.ReadAllLines(file);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to open client permission file " + ClientPermissionsFile, e);
return;
}
ClientPermissions.Clear();
foreach (string line in lines)
{
string[] separatedLine = line.Split('|');
if (separatedLine.Length < 3) continue;
string name = string.Join("|", separatedLine.Take(separatedLine.Length - 2));
string ip = separatedLine[separatedLine.Length - 2];
ClientPermissions permissions = Networking.ClientPermissions.None;
if (Enum.TryParse(separatedLine.Last(), out permissions))
{
ClientPermissions.Add(new SavedClientPermission(name, ip, permissions, new List<DebugConsole.Command>()));
}
}
}
public void SaveClientPermissions()
{
//delete old client permission file
if (File.Exists("Data/clientpermissions.txt"))
{
File.Delete("Data/clientpermissions.txt");
}
GameServer.Log("Saving client permissions", ServerLog.MessageType.ServerMessage);
XDocument doc = new XDocument(new XElement("ClientPermissions"));
foreach (SavedClientPermission clientPermission in ClientPermissions)
{
var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands));
if (matchingPreset != null && matchingPreset.Name == "None")
{
continue;
}
XElement clientElement = new XElement("Client",
new XAttribute("name", clientPermission.Name));
if (clientPermission.SteamID > 0)
{
clientElement.Add(new XAttribute("steamid", clientPermission.SteamID));
}
else
{
clientElement.Add(new XAttribute("endpoint", clientPermission.EndPoint));
}
if (matchingPreset == null)
{
clientElement.Add(new XAttribute("permissions", clientPermission.Permissions.ToString()));
if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands))
{
foreach (DebugConsole.Command command in clientPermission.PermittedCommands)
{
clientElement.Add(new XElement("command", new XAttribute("name", command.names[0])));
}
}
}
else
{
clientElement.Add(new XAttribute("preset", matchingPreset.Name));
}
doc.Root.Add(clientElement);
}
try
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineOnAttributes = true;
using (var writer = XmlWriter.Create(ClientPermissionsFile, settings))
{
doc.Save(writer);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving client permissions to " + ClientPermissionsFile + " failed", e);
}
}
}
}
@@ -0,0 +1,112 @@
using System.Linq;
namespace Barotrauma.Steam
{
partial class SteamManager
{
#region Server
private static void InitializeProjectSpecific() { isInitialized = true; }
private static void UpdateProjectSpecific(float deltaTime) { }
public static bool CreateServer(Networking.GameServer server, bool isPublic)
{
isInitialized = true;
Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma")
{
GamePort = (ushort)server.Port,
QueryPort = (ushort)server.QueryPort,
Secure = false
};
//options.QueryShareGamePort();
Steamworks.SteamServer.Init(AppID, options, false);
if (!Steamworks.SteamServer.IsValid)
{
Steamworks.SteamServer.Shutdown();
DebugConsole.ThrowError("Initializing Steam server failed.");
return false;
}
RefreshServerDetails(server);
server.ServerPeer.InitializeSteamServerCallbacks();
Steamworks.SteamServer.LogOnAnonymous();
return true;
}
public static bool RefreshServerDetails(Networking.GameServer server)
{
if (!isInitialized || !Steamworks.SteamServer.IsValid)
{
return false;
}
var contentPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent);
// These server state variables may be changed at any time. Note that there is no longer a mechanism
// to send the player count. The player count is maintained by steam and you should use the player
// creation/authentication functions to maintain your player count.
Steamworks.SteamServer.ServerName = server.ServerName;
Steamworks.SteamServer.MaxPlayers = server.ServerSettings.MaxPlayers;
Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword;
Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName ?? "";
Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText);
Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString());
Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString());
Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash)));
Steamworks.SteamServer.SetKey("contentpackageurl", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopUrl ?? "")));
Steamworks.SteamServer.SetKey("usingwhitelist", (server.ServerSettings.Whitelist != null && server.ServerSettings.Whitelist.Enabled).ToString());
Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString());
Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString());
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());
Steamworks.SteamServer.SetKey("allowspectating", server.ServerSettings.AllowSpectating.ToString());
Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString());
Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString());
Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString());
Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier);
Steamworks.SteamServer.DedicatedServer = true;
return true;
}
public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID)
{
if (!isInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam;
DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID);
Steamworks.BeginAuthResult startResult = Steamworks.SteamServer.BeginAuthSession(authTicketData, clientSteamID);
if (startResult != Steamworks.BeginAuthResult.OK)
{
DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")");
}
return startResult;
}
public static void StopAuthSession(ulong clientSteamID)
{
if (!isInitialized || !Steamworks.SteamServer.IsValid) return;
DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamID);
Steamworks.SteamServer.EndSession(clientSteamID);
}
public static bool CloseServer()
{
if (!isInitialized || !Steamworks.SteamServer.IsValid) return false;
Steamworks.SteamServer.Shutdown();
return true;
}
#endregion
}
}
@@ -0,0 +1,102 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.Networking
{
class VoipServer
{
private ServerPeer netServer;
private List<VoipQueue> queues;
private Dictionary<VoipQueue,DateTime> lastSendTime;
public VoipServer(ServerPeer server)
{
this.netServer = server;
queues = new List<VoipQueue>();
lastSendTime = new Dictionary<VoipQueue, DateTime>();
}
public void RegisterQueue(VoipQueue queue)
{
if (!queues.Contains(queue)) queues.Add(queue);
}
public void UnregisterQueue(VoipQueue queue)
{
if (queues.Contains(queue)) queues.Remove(queue);
}
public void SendToClients(List<Client> clients)
{
foreach (VoipQueue queue in queues)
{
if (queue.LastReadTime < DateTime.Now - VoipConfig.SEND_INTERVAL) { continue; }
if (lastSendTime.ContainsKey(queue))
{
if ((lastSendTime[queue] + VoipConfig.SEND_INTERVAL) > DateTime.Now) { continue; }
lastSendTime[queue] = DateTime.Now;
}
else
{
lastSendTime.Add(queue, DateTime.Now);
}
Client sender = clients.Find(c => c.VoipQueue == queue);
foreach (Client recipient in clients)
{
if (recipient == sender) { continue; }
if (!CanReceive(sender, recipient)) { continue; }
IWriteMessage msg = new WriteOnlyMessage();
msg.Write((byte)ServerPacketHeader.VOICE);
msg.Write((byte)queue.QueueID);
queue.Write(msg);
netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable);
}
}
}
private bool CanReceive(Client sender, Client recipient)
{
if (Screen.Selected != GameMain.GameScreen) { return true; }
//no-one can hear muted players
if (sender.Muted) { return false; }
bool recipientSpectating = recipient.Character == null || recipient.Character.IsDead;
bool senderSpectating = sender.Character == null || sender.Character.IsDead;
//TODO: only allow spectators to hear the voice chat if close enough to the speaker?
//non-spectators cannot hear spectators
if (senderSpectating && !recipientSpectating) { return false; }
//both spectating, no need to do radio/distance checks
if (recipientSpectating && senderSpectating) { return true; }
//spectators can hear non-spectators
if (!senderSpectating && recipientSpectating) { return true; }
//sender can't speak
if (sender.Character != null && sender.Character.SpeechImpediment >= 100.0f) { return false; }
//check if the message can be sent via radio
if (ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) &&
ChatMessage.CanUseRadio(recipient.Character, out WifiComponent recipientRadio))
{
if (recipientRadio.CanReceive(senderRadio)) { return true; }
}
//otherwise do a distance check
return ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange) < 1.0f;
}
}
}
@@ -0,0 +1,133 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Voting
{
public bool AllowSubVoting
{
get { return allowSubVoting; }
set { allowSubVoting = value; }
}
public bool AllowModeVoting
{
get { return allowModeVoting; }
set { allowModeVoting = value; }
}
public void ServerRead(IReadMessage inc, Client sender)
{
if (GameMain.Server == null || sender == null) return;
byte voteTypeByte = inc.ReadByte();
VoteType voteType = VoteType.Unknown;
try
{
voteType = (VoteType)voteTypeByte;
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to cast vote type \"" + voteTypeByte + "\"", e);
return;
}
switch (voteType)
{
case VoteType.Sub:
string subName = inc.ReadString();
Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName);
sender.SetVote(voteType, sub);
break;
case VoteType.Mode:
string modeIdentifier = inc.ReadString();
GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
if (!mode.Votable) break;
sender.SetVote(voteType, mode);
break;
case VoteType.EndRound:
if (!sender.HasSpawned) return;
sender.SetVote(voteType, inc.ReadBoolean());
GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote<bool>(VoteType.EndRound));
GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned);
break;
case VoteType.Kick:
byte kickedClientID = inc.ReadByte();
Client kicked = GameMain.Server.ConnectedClients.Find(c => c.ID == kickedClientID);
if (kicked != null && kicked.Connection != GameMain.Server.OwnerConnection && !kicked.HasKickVoteFrom(sender))
{
kicked.AddKickVote(sender);
Client.UpdateKickVotes(GameMain.Server.ConnectedClients);
GameMain.Server.SendChatMessage($"ServerMessage.HasVotedToKick~[initiator]={sender.Name}~[target]={kicked.Name}", ChatMessageType.Server, null);
}
break;
case VoteType.StartRound:
bool ready = inc.ReadBoolean();
if (ready != sender.GetVote<bool>(VoteType.StartRound))
{
sender.SetVote(VoteType.StartRound, ready);
GameServer.Log(sender.Name + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage);
}
break;
}
inc.ReadPadBits();
GameMain.Server.UpdateVoteStatus();
}
public void ServerWrite(IWriteMessage msg)
{
if (GameMain.Server == null) return;
msg.Write(allowSubVoting);
if (allowSubVoting)
{
List<Pair<object, int>> voteList = GetVoteList(VoteType.Sub, GameMain.Server.ConnectedClients);
msg.Write((byte)voteList.Count);
foreach (Pair<object, int> vote in voteList)
{
msg.Write((byte)vote.Second);
msg.Write(((Submarine)vote.First).Name);
}
}
msg.Write(AllowModeVoting);
if (allowModeVoting)
{
List<Pair<object, int>> voteList = GetVoteList(VoteType.Mode, GameMain.Server.ConnectedClients);
msg.Write((byte)voteList.Count);
foreach (Pair<object, int> vote in voteList)
{
msg.Write((byte)vote.Second);
msg.Write(((GameModePreset)vote.First).Identifier);
}
}
msg.Write(AllowEndVoting);
if (AllowEndVoting)
{
msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote<bool>(VoteType.EndRound)));
msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned));
}
msg.Write(AllowVoteKick);
var readyClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote<bool>(VoteType.StartRound));
msg.Write((byte)readyClients.Count);
foreach (Client c in readyClients)
{
msg.Write(c.ID);
}
msg.WritePadBits();
}
}
}
@@ -0,0 +1,205 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
namespace Barotrauma.Networking
{
partial class WhiteListedPlayer
{
private static UInt16 LastIdentifier = 0;
public WhiteListedPlayer(string name,string ip)
{
Name = name;
IP = ip;
UniqueIdentifier = LastIdentifier; LastIdentifier++;
}
}
partial class WhiteList
{
partial void InitProjSpecific()
{
if (!File.Exists(SavePath)) { return; }
string[] lines;
try
{
lines = File.ReadAllLines(SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to open whitelist in " + SavePath, e);
return;
}
foreach (string line in lines)
{
if (line[0] == '#')
{
string lineval = line.Substring(1, line.Length - 1);
Int32.TryParse(lineval, out int intVal);
if (lineval.ToLower() == "true" || intVal != 0)
{
Enabled = true;
}
else
{
Enabled = false;
}
}
else
{
string[] separatedLine = line.Split(',');
if (separatedLine.Length < 2) continue;
string name = string.Join(",", separatedLine.Take(separatedLine.Length - 1));
string ip = separatedLine.Last();
whitelistedPlayers.Add(new WhiteListedPlayer(name, ip));
}
}
}
public void Save()
{
GameServer.Log("Saving whitelist", ServerLog.MessageType.ServerMessage);
List<string> lines = new List<string>();
if (Enabled)
{
lines.Add("#true");
}
else
{
lines.Add("#false");
}
foreach (WhiteListedPlayer wlp in whitelistedPlayers)
{
lines.Add(wlp.Name + "," + wlp.IP);
}
try
{
File.WriteAllLines(SavePath, lines);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving the whitelist to " + SavePath + " failed", e);
}
}
public bool IsWhiteListed(string name, IPAddress address)
{
if (!Enabled) return true;
WhiteListedPlayer wlp = whitelistedPlayers.Find(p => p.Name == name);
if (wlp == null) return false;
if (!string.IsNullOrWhiteSpace(wlp.IP))
{
if (address.IsIPv4MappedToIPv6 && wlp.IP == address.MapToIPv4NoThrow().ToString())
{
return true;
}
else
{
return wlp.IP == address.ToString();
}
}
return true;
}
private void RemoveFromWhiteList(WhiteListedPlayer wlp)
{
GameServer.Log("Removing " + wlp.Name + " from whitelist", ServerLog.MessageType.ServerMessage);
whitelistedPlayers.Remove(wlp);
}
private void AddToWhiteList(string name, string ip)
{
if (string.IsNullOrWhiteSpace(name)) return;
if (whitelistedPlayers.Any(x => x.Name.ToLower() == name.ToLower() && x.IP == ip)) return;
whitelistedPlayers.Add(new WhiteListedPlayer(name, ip));
}
public void ServerAdminWrite(IWriteMessage outMsg, Client c)
{
if (!c.HasPermission(ClientPermissions.ManageSettings))
{
outMsg.Write(false); outMsg.WritePadBits();
return;
}
outMsg.Write(true);
outMsg.Write(c.Connection == GameMain.Server.OwnerConnection);
outMsg.Write(Enabled);
outMsg.WritePadBits();
outMsg.WriteVariableUInt32((UInt32)whitelistedPlayers.Count);
for (int i = 0; i < whitelistedPlayers.Count; i++)
{
WhiteListedPlayer whitelistedPlayer = whitelistedPlayers[i];
outMsg.Write(whitelistedPlayer.Name);
outMsg.Write(whitelistedPlayer.UniqueIdentifier);
if (c.Connection == GameMain.Server.OwnerConnection)
{
outMsg.Write(whitelistedPlayer.IP);
//outMsg.Write(whitelistedPlayer.SteamID); //TODO: add steamid to whitelisted players
}
}
}
public bool ServerAdminRead(IReadMessage incMsg, Client c)
{
if (!c.HasPermission(ClientPermissions.ManageSettings))
{
bool enabled = incMsg.ReadBoolean(); incMsg.ReadPadBits();
UInt16 removeCount = incMsg.ReadUInt16();
incMsg.BitPosition += removeCount * 4 * 8;
UInt16 addCount = incMsg.ReadUInt16();
for (int i = 0; i < addCount; i++)
{
incMsg.ReadString(); //skip name
incMsg.ReadString(); //skip ip
}
return false;
}
else
{
bool prevEnabled = Enabled;
bool enabled = incMsg.ReadBoolean(); incMsg.ReadPadBits();
Enabled = enabled;
UInt16 removeCount = incMsg.ReadUInt16();
for (int i = 0; i < removeCount; i++)
{
UInt16 id = incMsg.ReadUInt16();
WhiteListedPlayer whitelistedPlayer = whitelistedPlayers.Find(p => p.UniqueIdentifier == id);
if (whitelistedPlayer != null)
{
GameServer.Log(c.Name + " removed " + whitelistedPlayer.Name + " from whitelist (" + whitelistedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage);
RemoveFromWhiteList(whitelistedPlayer);
}
}
UInt16 addCount = incMsg.ReadUInt16();
for (int i = 0; i < addCount; i++)
{
string name = incMsg.ReadString();
string ip = incMsg.ReadString();
GameServer.Log(c.Name + " added " + name + " to whitelist (" + ip + ")", ServerLog.MessageType.ConsoleUsage);
AddToWhiteList(name, ip);
}
bool changed = removeCount > 0 || addCount > 0 || prevEnabled != enabled;
if (changed) { Save(); }
return changed;
}
}
}
}
@@ -0,0 +1,50 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
partial class PhysicsBody
{
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity;
msg.Write(SimPosition.X);
msg.Write(SimPosition.Y);
#if DEBUG
if (Math.Abs(FarseerBody.LinearVelocity.X) > MaxVel ||
Math.Abs(FarseerBody.LinearVelocity.Y) > MaxVel)
{
DebugConsole.ThrowError("Item velocity out of range (" + FarseerBody.LinearVelocity + ")");
}
#endif
msg.Write(FarseerBody.Awake);
msg.Write(FarseerBody.FixedRotation);
if (!FarseerBody.FixedRotation)
{
msg.WriteRangedSingle(MathUtils.WrapAngleTwoPi(FarseerBody.Rotation), 0.0f, MathHelper.TwoPi, 8);
}
if (FarseerBody.Awake)
{
FarseerBody.Enabled = true;
FarseerBody.LinearVelocity = new Vector2(
MathHelper.Clamp(FarseerBody.LinearVelocity.X, -MaxVel, MaxVel),
MathHelper.Clamp(FarseerBody.LinearVelocity.Y, -MaxVel, MaxVel));
msg.WriteRangedSingle(FarseerBody.LinearVelocity.X, -MaxVel, MaxVel, 12);
msg.WriteRangedSingle(FarseerBody.LinearVelocity.Y, -MaxVel, MaxVel, 12);
if (!FarseerBody.FixedRotation)
{
FarseerBody.AngularVelocity = MathHelper.Clamp(FarseerBody.AngularVelocity, -MaxAngularVel, MaxAngularVel);
msg.WriteRangedSingle(FarseerBody.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8);
}
}
msg.WritePadBits();
}
}
}
@@ -0,0 +1,145 @@
#region Using Statements
using Barotrauma.Steam;
using GameAnalyticsSDK.Net;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
#endregion
namespace Barotrauma
{
/// <summary>
/// The main class.
/// </summary>
public static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
GameMain game = null;
#if !DEBUG
try
{
#endif
Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version +
" (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")");
game = new GameMain(args);
game.Run();
if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); }
SteamManager.ShutDown();
#if !DEBUG
}
catch (Exception e)
{
CrashDump(game, "servercrashreport.log", e);
GameMain.Server?.NotifyCrash();
}
#endif
}
static void CrashDump(GameMain game, string filePath, Exception exception)
{
try
{
GameMain.Server?.ServerSettings?.SaveSettings();
GameMain.Server?.ServerSettings?.BanList.Save();
if (GameMain.Server?.ServerSettings?.KarmaPreset == "custom")
{
GameMain.Server?.KarmaManager?.SaveCustomPreset();
GameMain.Server?.KarmaManager?.Save();
}
}
//gotta catch them all, we don't want to crash while writing a crash report
catch (Exception e)
{
string errorMsg = "Exception thrown while writing a crash report: " + e.Message + "\n" + e.StackTrace;
GameAnalyticsManager.AddErrorEventOnce("CrashDump:FailedToSaveSettings", EGAErrorSeverity.Error, errorMsg);
}
int existingFiles = 0;
string originalFilePath = filePath;
while (File.Exists(filePath))
{
existingFiles++;
filePath = Path.GetFileNameWithoutExtension(originalFilePath) + " (" + (existingFiles + 1) + ")" + Path.GetExtension(originalFilePath);
}
StreamWriter sw = new StreamWriter(filePath);
StringBuilder sb = new StringBuilder();
sb.AppendLine("Barotrauma Dedicated Server crash report (generated on " + DateTime.Now + ")");
sb.AppendLine("\n");
sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! ");
sb.AppendLine("\n");
sb.AppendLine("Game version " + GameMain.Version +
" (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")");
sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name))));
sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed));
sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash + ")"));
sb.AppendLine("Selected screen: " + (Screen.Selected == null ? "None" : Screen.Selected.ToString()));
if (GameMain.Server != null)
{
sb.AppendLine("Server (" + (GameMain.Server.GameStarted ? "Round had started)" : "Round hadn't been started)"));
}
sb.AppendLine("\n");
sb.AppendLine("System info:");
sb.AppendLine(" Operating system: " + System.Environment.OSVersion + (System.Environment.Is64BitOperatingSystem ? " 64 bit" : " x86"));
sb.AppendLine("\n");
sb.AppendLine("Exception: " + exception.Message + " (" + exception.GetType().ToString() + ")");
sb.AppendLine("Target site: " +exception.TargetSite.ToString());
sb.AppendLine("Stack trace: ");
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:");
DebugConsole.Clear();
for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i-- )
{
sb.AppendLine(" "+DebugConsole.Messages[i].Time+" - "+DebugConsole.Messages[i].Text);
}
string crashReport = sb.ToString();
Console.ForegroundColor = ConsoleColor.Red;
Console.Write(crashReport);
sw.WriteLine(sb.ToString());
sw.Close();
if (GameSettings.SendUserStatistics)
{
GameAnalytics.AddErrorEvent(EGAErrorSeverity.Critical, crashReport);
GameAnalytics.OnQuit();
Console.Write("A crash report (\"crashreport.log\") was saved in the root folder of the game and sent to the developers.");
}
else
{
Console.Write("A crash report(\"crashreport.log\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" +
" if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/");
}
SteamManager.ShutDown();
}
}
}
@@ -0,0 +1,211 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class NetLobbyScreen : Screen
{
private Submarine selectedSub;
private Submarine selectedShuttle;
public Submarine SelectedSub
{
get { return selectedSub; }
set
{
selectedSub = value;
lastUpdateID++;
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true;
}
}
}
public Submarine SelectedShuttle
{
get { return selectedShuttle; }
set { selectedShuttle = value; lastUpdateID++; }
}
public GameModePreset[] GameModes { get; }
private int selectedModeIndex;
public int SelectedModeIndex
{
get { return selectedModeIndex; }
set
{
lastUpdateID++;
selectedModeIndex = MathHelper.Clamp(value, 0, GameModes.Length - 1);
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.GameModeIdentifier = SelectedModeIdentifier;
GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true;
}
}
}
public string SelectedModeIdentifier
{
get { return GameModes[SelectedModeIndex].Identifier; }
set
{
for (int i = 0; i < GameModes.Length; i++)
{
if (GameModes[i].Identifier.ToLower() == value.ToLower())
{
SelectedModeIndex = i;
break;
}
}
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.GameModeIdentifier = SelectedModeIdentifier;
GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true;
}
}
}
public GameModePreset SelectedMode
{
get { return GameModes[SelectedModeIndex]; }
}
private MissionType missionType;
public MissionType MissionType
{
get { return missionType; }
set
{
lastUpdateID++;
missionType = value;
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.MissionType = missionType.ToString();
}
}
}
public string MissionTypeName
{
get { return missionType.ToString(); }
set
{
Enum.TryParse(value, out MissionType type);
MissionType = type;
}
}
public void ChangeServerName(string n)
{
GameMain.Server.ServerSettings.ServerName = n; lastUpdateID++;
}
public void ChangeServerMessage(string m)
{
GameMain.Server.ServerSettings.ServerMessageText = m; lastUpdateID++;
}
public List<JobPrefab> JobPreferences
{
get
{
return null;
}
}
public NetLobbyScreen()
{
LevelSeed = ToolBox.RandomSeed(8);
subs = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList();
if (subs == null || subs.Count() == 0)
{
throw new Exception("No submarines are available.");
}
selectedSub = subs.FirstOrDefault(s => !s.HasTag(SubmarineTag.Shuttle));
if (selectedSub == null)
{
//no subs available, use a shuttle
DebugConsole.ThrowError("No full-size submarines available - choosing a shuttle as the main submarine.");
selectedSub = subs[0];
}
selectedShuttle = subs.First(s => s.HasTag(SubmarineTag.Shuttle));
if (selectedShuttle == null)
{
//no shuttles available, use a sub
DebugConsole.ThrowError("No shuttles available - choosing a full-size submarine as the shuttle.");
selectedShuttle = subs[0];
}
DebugConsole.NewMessage("Selected sub: " + SelectedSub.Name, Color.White);
DebugConsole.NewMessage("Selected shuttle: " + SelectedShuttle.Name, Color.White);
GameModes = GameModePreset.List.ToArray();
}
private List<Submarine> subs;
public List<Submarine> GetSubList()
{
return subs;
}
public string LevelSeed
{
get
{
return levelSeed;
}
set
{
if (levelSeed == value) return;
lastUpdateID++;
levelSeed = value;
LocationType.Random(new MTRandom(ToolBox.StringToInt(levelSeed))); //call to sync up with clients
}
}
public void ToggleCampaignMode(bool enabled)
{
for (int i = 0; i < GameModes.Length; i++)
{
if ((GameModes[i].Identifier == "multiplayercampaign") == enabled)
{
selectedModeIndex = i;
break;
}
}
lastUpdateID++;
}
public override void Select()
{
base.Select();
GameMain.Server.ServerSettings.Voting.ResetVotes(GameMain.Server.ConnectedClients);
}
public void RandomizeSettings()
{
if (GameMain.Server.ServerSettings.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8);
if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random)
{
var nonShuttles = Submarine.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus)).ToList();
SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)];
}
if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random)
{
var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m.Identifier != "multiplayercampaign");
SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier;
}
}
}
}
@@ -0,0 +1,14 @@
using System;
namespace Barotrauma
{
class UnimplementedScreen : Screen
{
public static readonly UnimplementedScreen Instance = new UnimplementedScreen();
public override void Select()
{
throw new Exception("Tried to select unimplemented screen");
}
}
}
@@ -0,0 +1,63 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SqlServer.Server;
namespace Barotrauma
{
partial class Traitor
{
public abstract class Goal
{
public HashSet<Traitor> Traitors { get; } = new HashSet<Traitor>();
public TraitorMission Mission { get; internal set; }
public virtual string StatusTextId { get; set; } = "TraitorGoalStatusTextFormat";
public virtual string InfoTextId { get; set; } = null;
public virtual string CompletedTextId { get; set; } = null;
public virtual string StatusValueTextId => IsCompleted ? "complete" : "inprogress";
public virtual IEnumerable<string> StatusTextKeys => new [] { "[infotext]", "[status]" };
public virtual IEnumerable<string> StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) };
public virtual IEnumerable<string> InfoTextKeys => new string[] { };
public virtual IEnumerable<string> InfoTextValues(Traitor traitor) => new string[] { };
public virtual IEnumerable<string> CompletedTextKeys => new string[] { };
public virtual IEnumerable<string> CompletedTextValues(Traitor traitor) => new string[] { };
protected virtual string FormatText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => TextManager.FormatServerMessageWithGenderPronouns(traitor?.Character?.Info?.Gender ?? Gender.None, textId, keys, values);
protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => FormatText(traitor, textId, keys, values);
protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => FormatText(traitor, textId, keys, values);
protected internal virtual string GetCompletedText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => FormatText(traitor, textId, keys, values);
public virtual string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor));
public virtual string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor));
public virtual string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor);
public abstract bool IsCompleted { get; }
public virtual bool IsStarted(Traitor traitor) => Traitors.Contains(traitor);
public virtual bool CanBeCompleted(ICollection<Traitor> traitors) => !Traitors.Any(traitor => traitor.Character?.IsDead ?? true);
public virtual bool IsEnemy(Character character) => false;
public virtual bool IsAllowedToDamage(Structure structure) => false;
public virtual bool Start(Traitor traitor)
{
Traitors.Add(traitor);
return true;
}
public virtual void Update(float deltaTime)
{
}
protected Goal()
{
}
}
}
}
@@ -0,0 +1,105 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalDestroyItemsWithTag : Goal
{
private readonly string tag;
private readonly bool matchIdentifier;
private readonly bool matchTag;
private readonly bool matchInventory;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]", "[tag]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", DestroyPercent * 100.0f), tagPrefabName ?? "" });
private readonly float destroyPercent;
private float DestroyPercent => destroyPercent;
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private int totalCount = 0;
private int targetCount = 0;
private string tagPrefabName = null;
private int CountMatchingItems()
{
int result = 0;
foreach (var item in Item.ItemList)
{
if (!matchInventory && Traitors.All(traitor => item.FindParentInventory(inventory => inventory.Owner is Character && inventory.Owner != traitor.Character) != null))
{
continue;
}
if (item.Submarine == null)
{
if (!(item.ParentInventory?.Owner is Character)) { continue; }
}
else
{
if (Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID)) { continue; }
}
if (item.Condition <= 0.0f)
{
continue;
}
var identifierMatches = matchIdentifier && item.prefab.Identifier == tag;
if (identifierMatches && tagPrefabName == null)
{
var textId = item.Prefab.GetItemNameTextId();
tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name;
}
if (identifierMatches || (matchTag && item.HasTag(tag)))
{
++result;
}
}
// Quick fix
if (tagPrefabName == null && matchIdentifier)
{
tagPrefabName = TextManager.FormatServerMessage($"entityname.{tag}");
}
return result;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = CountMatchingItems() <= targetCount;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
totalCount = CountMatchingItems();
if (totalCount <= 0)
{
return false;
}
targetCount = (int)((1.0f - destroyPercent) * totalCount - 0.5f);
return true;
}
public GoalDestroyItemsWithTag(string tag, float destroyPercent, bool matchTag, bool matchIdentifier, bool matchInventory) : base()
{
InfoTextId = "TraitorGoalDestroyItems";
this.tag = tag;
this.destroyPercent = destroyPercent;
this.matchTag = matchTag;
this.matchIdentifier = matchIdentifier;
this.matchInventory = matchInventory;
}
}
}
}
@@ -0,0 +1,162 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalEntityTransformation : Goal
{
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[catalystitem]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { catalystItemName });
private bool isCompleted;
public override bool IsCompleted => isCompleted;
private string catalystItemIdentifier, catalystItemName;
private Vector2 activeEntitySavedPosition;
private Entity activeEntity;
private int activeEntityIndex;
private const float gracePeriod = 1f;
private const float graceDistance = 200f;
private float graceTimer;
private double transformationTime;
private enum EntityTypes { Character, Item }
private string[] entities;
private EntityTypes[] entityTypes;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = HasTransformed(deltaTime);
}
public override bool CanBeCompleted(ICollection<Traitor> traitors)
{
return graceTimer <= gracePeriod;
}
private bool HasTransformed(float deltaTime)
{
if (activeEntity != null && !activeEntity.Removed)
{
activeEntitySavedPosition = activeEntity.WorldPosition;
}
else
{
if (transformationTime == 0)
{
graceTimer = 0.0f;
activeEntityIndex++;
transformationTime = Timing.TotalTime;
}
graceTimer += deltaTime;
switch (entityTypes[activeEntityIndex])
{
case EntityTypes.Character:
foreach (Character character in Character.CharacterList)
{
if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < transformationTime)
{
continue;
}
if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase) && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance)
{
activeEntity = character;
transformationTime = 0.0;
return activeEntityIndex == entities.Length - 1;
}
}
break;
case EntityTypes.Item:
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID) || item.SpawnTime + gracePeriod < transformationTime)
{
continue;
}
if (item.prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance)
{
activeEntity = item;
transformationTime = 0.0;
return activeEntityIndex == entities.Length - 1;
}
}
break;
}
}
return false;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
catalystItemName = TextManager.FormatServerMessage($"entityname.{catalystItemIdentifier}");
activeEntity = null;
activeEntityIndex = 0;
switch (entityTypes[activeEntityIndex])
{
case EntityTypes.Character:
foreach (Character character in Character.CharacterList)
{
if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase))
{
activeEntity = character;
break;
}
}
break;
case EntityTypes.Item:
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (item.prefab.Identifier.Equals(entities[0], StringComparison.OrdinalIgnoreCase))
{
activeEntity = item;
break;
}
}
break;
}
graceTimer = 0.0f;
return activeEntity != null;
}
public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base()
{
this.entities = entities;
this.entityTypes = new EntityTypes[entityTypes.Length];
for (int i = 0; i < this.entityTypes.Length; i++)
{
this.entityTypes[i] = (EntityTypes)Enum.Parse(typeof(EntityTypes), entityTypes[i], true);
}
this.catalystItemIdentifier = catalystItemIdentifier;
}
}
}
}
@@ -0,0 +1,235 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public class GoalFindItem : HumanoidGoal
{
private readonly TraitorMission.CharacterFilter filter;
private readonly string identifier;
private readonly bool preferNew;
private readonly bool allowNew;
private readonly bool allowExisting;
private readonly HashSet<string> allowedContainerIdentifiers = new HashSet<string>();
private ItemPrefab targetPrefab;
private ItemPrefab containedPrefab;
private Item targetContainer;
private Item target;
private HashSet<Item> existingItems = new HashSet<Item>();
private string targetNameText;
private string targetContainerNameText;
private string targetHullNameText;
private float percentage;
private int spawnAmount = 1;
private const string itemContainerId = "toolbox";
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" });
public override bool IsCompleted => target != null && Traitors.Any(traitor => traitor.Character.HasItem(target));
public override bool CanBeCompleted(ICollection<Traitor> traitors)
{
if (!base.CanBeCompleted(traitors))
{
return false;
}
if (target == null)
{
var targetPrefabCandidate = FindItemPrefab(identifier);
return targetPrefabCandidate != null && FindTargetContainer(traitors, targetPrefabCandidate) != null;
}
if (target.Removed)
{
return false;
}
if (target.Submarine == null)
{
if (!(target.ParentInventory?.Owner is Character))
{
return false;
}
}
else
{
if (Traitors.All(traitor => target.Submarine.TeamID != traitor.Character.TeamID))
{
return false;
}
}
return true;
}
public override bool IsEnemy(Character character) => base.IsEnemy(character) || (target != null && target.FindParentInventory(inventory => inventory == character.Inventory) != null);
protected ItemPrefab FindItemPrefab(string identifier)
{
return (ItemPrefab)MapEntityPrefab.List.FirstOrDefault(prefab => prefab is ItemPrefab && prefab.Identifier == identifier);
}
protected Item FindRandomContainer(ICollection<Traitor> traitors, ItemPrefab targetPrefabCandidate, bool includeNew, bool includeExisting)
{
List<Item> suitableItems = new List<Item>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine == null || traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID))
{
continue;
}
if (item.GetComponent<ItemContainer>() != null && allowedContainerIdentifiers.Contains(item.prefab.Identifier))
{
if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null))
{
suitableItems.Add(item);
}
}
}
if (suitableItems.Count == 0) { return null; }
return suitableItems[TraitorManager.RandomInt(suitableItems.Count)];
}
protected Item FindTargetContainer(ICollection<Traitor> traitors, ItemPrefab targetPrefabCandidate)
{
Item result = null;
if (preferNew)
{
result = FindRandomContainer(traitors, targetPrefabCandidate, true, false);
}
if (result == null)
{
result = FindRandomContainer(traitors, targetPrefabCandidate, allowNew, allowExisting);
}
if (result == null)
{
return null;
}
if (allowNew && !result.OwnInventory.IsFull())
{
return result;
}
if (allowExisting && result.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)
{
return result;
}
return null;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
if (targetPrefab != null)
{
return true;
}
string targetPrefabTextId;
if (percentage > 0f)
{
spawnAmount = (int)Math.Floor(Character.CharacterList.FindAll(c => c.TeamID == traitor.Character.TeamID && c != traitor.Character && !c.IsDead && (filter == null || filter(c))).Count * percentage);
}
if (spawnAmount > 1 && allowNew)
{
containedPrefab = FindItemPrefab(identifier);
targetPrefab = FindItemPrefab(itemContainerId);
if (containedPrefab == null || targetPrefab == null)
{
return false;
}
targetPrefabTextId = containedPrefab.GetItemNameTextId();
}
else
{
spawnAmount = 1;
containedPrefab = null;
targetPrefab = FindItemPrefab(identifier);
if (targetPrefab == null)
{
return false;
}
targetPrefabTextId = targetPrefab.GetItemNameTextId();
}
targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name;
targetContainer = FindTargetContainer(Traitors, targetPrefab);
if (targetContainer == null)
{
targetPrefab = null;
targetContainer = null;
return false;
}
var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId();
targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name;
var targetHullTextId = targetContainer.CurrentHull?.prefab.GetHullNameTextId();
targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName ?? "";
if (allowNew && !targetContainer.OwnInventory.IsFull())
{
existingItems.Clear();
foreach (var item in targetContainer.OwnInventory.Items)
{
existingItems.Add(item);
}
Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory);
target = null;
}
else if (allowExisting)
{
target = targetContainer.OwnInventory.FindItemByIdentifier(targetPrefab.Identifier);
}
else
{
targetPrefab = null;
targetContainer = null;
return false;
}
return true;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (target == null)
{
target = targetContainer.OwnInventory.Items.FirstOrDefault(item => item != null && item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item));
if (target != null)
{
if (containedPrefab != null)
{
for (int i = 0; i < spawnAmount; i++)
{
Entity.Spawner.AddToSpawnQueue(containedPrefab, target.OwnInventory);
}
}
existingItems.Clear();
}
}
}
public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params string[] allowedContainerIdentifiers)
{
this.filter = filter;
this.identifier = identifier;
this.preferNew = preferNew;
this.allowNew = allowNew;
this.allowExisting = allowExisting;
this.percentage = percentage / 100f;
this.allowedContainerIdentifiers.UnionWith(allowedContainerIdentifiers);
}
}
}
}
@@ -0,0 +1,46 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalFloodPercentOfSub : Goal
{
private readonly float minimumFloodingAmount;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[percentage]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { string.Format("{0:0}", minimumFloodingAmount * 100.0f) });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
var validHullsCount = 0;
var floodingAmount = 0.0f;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine == null || hull.Submarine.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; }
if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; }
++validHullsCount;
floodingAmount += hull.WaterVolume / hull.Volume;
}
if (validHullsCount > 0)
{
floodingAmount /= validHullsCount;
}
isCompleted = floodingAmount >= minimumFloodingAmount;
}
public GoalFloodPercentOfSub(float minimumFloodingAmount) : base()
{
InfoTextId = "TraitorGoalFloodPercentOfSub";
this.minimumFloodingAmount = minimumFloodingAmount;
}
}
}
}
@@ -0,0 +1,78 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalInjectTarget : Goal
{
public TraitorMission.CharacterFilter Filter { get; private set; }
public List<Character> Targets { get; private set; }
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[poison]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", poisonName });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character));
private string poisonId;
private string afflictionId;
private string poisonName;
private int targetCount;
private float targetPercentage;
private bool[] targetWasInfected;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = WereAllTargetsInfected();
}
private bool WereAllTargetsInfected()
{
for (int i = 0; i < targetWasInfected.Length; i++)
{
if (targetWasInfected[i]) continue;
targetWasInfected[i] = Targets[i].CharacterHealth.GetAffliction(afflictionId) != null;
}
return targetWasInfected.All(t => t == true);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
poisonName = TextManager.FormatServerMessage(poisonId) ?? poisonId;
Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage);
targetWasInfected = new bool[Targets.Count];
return Targets != null && !Targets.All(t => t.IsDead);
}
public GoalInjectTarget(TraitorMission.CharacterFilter filter, string poisonId, string afflictionId, int targetCount, float targetPercentage) : base()
{
Filter = filter;
this.poisonId = poisonId;
this.afflictionId = afflictionId;
this.targetCount = targetCount;
this.targetPercentage = targetPercentage / 100f;
if (this.targetPercentage < 1.0f)
{
InfoTextId = "traitorgoalpoisoninfo";
}
else
{
InfoTextId = "traitorgoalpoisoneveryoneinfo";
}
}
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalKeepTransformedAlive : Goal
{
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[speciesname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetCharacterName });
public override bool IsCompleted => isCompleted;
private bool isCompleted;
private const float gracePeriod = 1f;
private string speciesId;
private string targetCharacterName;
private Character targetCharacter;
private float timer;
public override bool CanBeCompleted(ICollection<Traitor> traitors)
{
return timer < gracePeriod || targetCharacter != null && !targetCharacter.IsDead;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (timer <= gracePeriod)
{
timer += deltaTime;
}
isCompleted = targetCharacter != null && !targetCharacter.IsDead && timer >= gracePeriod;
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
var startTime = Timing.TotalTime;
foreach (Character character in Character.CharacterList)
{
if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < startTime)
{
continue;
}
if (character.SpeciesName.Equals(speciesId, StringComparison.OrdinalIgnoreCase))
{
targetCharacter = character;
break;
}
}
targetCharacterName = TextManager.FormatServerMessage($"character.{speciesId}").ToLowerInvariant();
return targetCharacter != null;
}
public GoalKeepTransformedAlive(string speciesId) : base()
{
this.speciesId = speciesId.ToLowerInvariant();
}
}
}
}
@@ -0,0 +1,163 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalKillTarget : Goal
{
public TraitorMission.CharacterFilter Filter { get; private set; }
public List<Character> Targets { get; private set; }
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[]
{ traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath(), targetHull != null ? TextManager.Get($"roomname.{targetHull}") : string.Empty });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character));
private CauseOfDeathType requiredCauseOfDeath;
private string afflictionId;
private string targetHull;
private int targetCount;
private float targetPercentage;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = DoesDeathMatchCriteria();
}
private bool DoesDeathMatchCriteria()
{
if (Targets == null || Targets.Any(t => !t.IsDead)) return false;
bool typeMatch = false;
for (int i = 0; i < Targets.Count; i++)
{
// No specified cause of death required or missing cause of death
if (requiredCauseOfDeath == CauseOfDeathType.Unknown || Targets[i].CauseOfDeath == null)
{
typeMatch = true;
}
else
{
switch (Targets[i].CauseOfDeath.Type)
{
// If a cause of death is labeled as unknown, side with the traitor and accept this regardless of the required type
case CauseOfDeathType.Unknown:
typeMatch = true;
break;
case CauseOfDeathType.Pressure:
case CauseOfDeathType.Suffocation:
case CauseOfDeathType.Drowning:
typeMatch = requiredCauseOfDeath == Targets[i].CauseOfDeath.Type;
break;
case CauseOfDeathType.Affliction:
typeMatch = Targets[i].CauseOfDeath.Type == requiredCauseOfDeath && Targets[i].CauseOfDeath.Affliction.Identifier == afflictionId;
break;
case CauseOfDeathType.Disconnected:
typeMatch = false;
break;
}
}
if (targetHull != null)
{
if (Targets[i].CurrentHull != null)
{
if (typeMatch && Targets[i].CurrentHull.RoomName == targetHull || Targets[i].CurrentHull.RoomName.Contains(targetHull))
{
continue;
}
else
{
return false;
}
}
else
{
// Outside the submarine, not supported for now
return false;
}
}
else
{
if (typeMatch)
{
continue;
}
else
{
return false;
}
}
}
return true;
}
private string GetCauseOfDeath()
{
if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty)
{
return requiredCauseOfDeath.ToString().ToLower();
}
else
{
return TextManager.Get($"afflictionname.{afflictionId}").ToLower();
}
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage);
return Targets != null && !Targets.All(t => t.IsDead);
}
public GoalKillTarget(TraitorMission.CharacterFilter filter, CauseOfDeathType requiredCauseOfDeath, string afflictionId, string targetHull, int targetCount, float targetPercentage) : base()
{
Filter = filter;
this.requiredCauseOfDeath = requiredCauseOfDeath;
this.afflictionId = afflictionId;
this.targetHull = targetHull;
this.targetCount = targetCount;
this.targetPercentage = targetPercentage / 100f;
if (this.targetPercentage < 1f)
{
if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull == null)
{
InfoTextId = "traitorgoalkilltargetinfo";
}
else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull == null)
{
InfoTextId = "traitorgoalkilltargetinfowithcause";
}
else if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull != null)
{
InfoTextId = "traitorgoalkilltargetinfowithhull";
}
else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull != null)
{
InfoTextId = "traitorgoalkilltargetinfowithcauseandhull";
}
}
else
{
InfoTextId = "traitorgoalkilleveryoneinfo";
}
}
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalReachDistanceFromSub : Goal
{
private readonly float requiredDistance;
private readonly float requiredDistanceSqr;
private float requiredDistanceInMeters;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistanceInMeters:0.00}" });
public override bool IsCompleted
{
get
{
return Traitors.Any(traitor =>
{
Submarine ownSub = null;
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] != null && Submarine.MainSubs[i].TeamID == traitor.Character.TeamID)
{
ownSub = Submarine.MainSubs[i];
break;
}
}
if (ownSub == null) return false;
var characterPosition = traitor.Character.WorldPosition;
var submarinePosition = ownSub.WorldPosition;
var distance = Vector2.DistanceSquared(characterPosition, submarinePosition);
return distance >= requiredDistanceSqr;
});
}
}
public GoalReachDistanceFromSub(float requiredDistance) : base()
{
InfoTextId = "TraitorGoalReachDistanceFromSub";
requiredDistanceInMeters = requiredDistance;
this.requiredDistance = requiredDistance / Physics.DisplayToRealWorldRatio;
requiredDistanceSqr = this.requiredDistance * this.requiredDistance;
}
}
}
}
@@ -0,0 +1,70 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public class GoalReplaceInventory : HumanoidGoal
{
private readonly HashSet<string> sabotageContainerIds = new HashSet<string>();
private readonly HashSet<string> validReplacementIds = new HashSet<string>();
private readonly float replaceAmount;
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
public override IEnumerable<string> StatusTextKeys => base.StatusTextKeys.Concat(new string[] { "[percentage]" });
public override IEnumerable<string> StatusTextValues(Traitor traitor) => base.StatusTextValues(traitor).Concat(new string[] { string.Format("{0:0}", replaceAmount * 100.0f) });
public override void Update(float deltaTime)
{
base.Update(deltaTime);
int totalAmount = 0, replacedAmount = 0;
foreach (var item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(traitor => item.Submarine.TeamID != traitor.Character.TeamID))
{
continue;
}
if (item.FindParentInventory(inventory => inventory.Owner is Character) != null)
{
continue;
}
if (sabotageContainerIds.Contains(item.prefab.Identifier))
{
++totalAmount;
if (item.OwnInventory.Items.Length <= 0 || item.OwnInventory.Items.All(containedItem => containedItem != null && !validReplacementIds.Contains(containedItem.Prefab.Identifier)))
{
continue;
}
++replacedAmount;
}
}
isCompleted = replacedAmount >= (int)(replaceAmount * totalAmount + 0.5f);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
if (sabotageContainerIds.Count <= 0 || validReplacementIds.Count <= 0)
{
return false;
}
return true;
}
public GoalReplaceInventory(string[] containerIds, string[] replacementIds, float replaceAmount)
{
sabotageContainerIds.UnionWith(containerIds);
validReplacementIds.UnionWith(replacementIds);
this.replaceAmount = replaceAmount;
}
}
}
}
@@ -0,0 +1,62 @@
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalSabotageItems : HumanoidGoal
{
private readonly string tag;
private readonly float conditionThreshold;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[tag]", "[target]", "[threshold]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { tag ?? "", targetItemPrefabName ?? "", string.Format("{0:0}", conditionThreshold) });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private readonly List<Item> targetItems = new List<Item>();
private string targetItemPrefabName = null;
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
foreach (var item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (item.Condition > conditionThreshold && (item.Prefab?.Identifier == tag || item.HasTag(tag)))
{
targetItems.Add(item);
}
}
if (targetItems.Count > 0)
{
var textId = targetItems[0].Prefab.GetItemNameTextId();
targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name;
}
return targetItems.Count > 0;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = targetItems.All(item => item.Condition <= conditionThreshold);
}
public GoalSabotageItems(string tag, float conditionThreshold) : base()
{
this.tag = tag;
this.conditionThreshold = conditionThreshold;
InfoTextId = "TraitorGoalSabotageInfo";
}
}
}
}
@@ -0,0 +1,96 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalUnwiring : HumanoidGoal
{
private readonly string tag;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[connectionname]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetItemPrefabName ?? "", targetConnectionDisplayName ?? targetConnectionName });
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private readonly List<ConnectionPanel> targetConnectionPanels = new List<ConnectionPanel>();
private string targetItemPrefabName;
private string targetConnectionName;
private string targetConnectionDisplayName;
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
foreach (var item in Item.ItemList)
{
if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID))
{
continue;
}
if (item.Prefab?.Identifier == tag || item.HasTag(tag))
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null)
{
targetConnectionPanels.Add(connectionPanel);
}
}
}
if (targetConnectionPanels.Count > 0)
{
var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId();
targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name;
}
return targetConnectionPanels.Count > 0;
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isCompleted = AreTargetsUnwired();
}
private bool AreTargetsUnwired()
{
for (int i = 0; i < targetConnectionPanels.Count; i++)
{
for (int j = 0; j < targetConnectionPanels[i].Connections.Count; j++)
{
if (targetConnectionPanels[i].Connections[j] == null || targetConnectionPanels[i].Connections[j].Wires == null) continue;
if (targetConnectionName != string.Empty)
{
if (targetConnectionPanels[i].Connections[j].Name != targetConnectionName) continue;
}
if (!targetConnectionPanels[i].Connections[j].Wires.All(w => w == null)) return false;
}
}
return true;
}
public GoalUnwiring(string tag, string targetConnectionName, string targetConnectionDisplayTag) : base()
{
this.tag = tag;
this.targetConnectionName = targetConnectionName;
if (targetConnectionDisplayTag != string.Empty)
{
targetConnectionDisplayName = TextManager.FormatServerMessage(targetConnectionDisplayTag);
InfoTextId = "TraitorGoalUnwireInfo";
}
else
{
InfoTextId = "TraitorGoalUnwireAllInfo";
}
}
}
}
}
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalWaitForTraitors : Goal
{
private readonly int requiredCount;
private int count = 0;
public override bool IsCompleted => count >= requiredCount;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[remaining]", "[count]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredCount - count}", $"{requiredCount}" });
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
++count;
return true;
}
public GoalWaitForTraitors(int requiredCount) : base()
{
this.requiredCount = requiredCount;
InfoTextId = "TraitorGoalWaitForTraitorsInfoText";
}
}
}
}
@@ -0,0 +1,17 @@
namespace Barotrauma
{
partial class Traitor
{
public abstract class HumanoidGoal : Goal
{
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
return traitor?.Character?.IsHumanoid ?? false;
}
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalHasDuration : Modifier
{
private readonly float requiredDuration;
private readonly bool countTotalDuration;
private readonly string durationInfoTextId;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString() });
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
{
var infoText = base.GetInfoText(traitor, textId, keys, values);
return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, new[] { "[infotext]", "[duration]" }, new[] { infoText, requiredDuration.ToString() }) : infoText;
}
private bool isCompleted = false;
public override bool IsCompleted => isCompleted;
private float remainingDuration = float.NaN;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (Goal.IsCompleted)
{
if (!float.IsNaN(remainingDuration))
{
remainingDuration -= deltaTime;
}
else
{
remainingDuration = requiredDuration;
}
isCompleted |= remainingDuration <= 0.0f;
}
else if (!countTotalDuration)
{
remainingDuration = float.NaN;
}
}
public GoalHasDuration(Goal goal, float requiredDuration, bool countTotalDuration, string durationInfoTextId) : base(goal)
{
this.requiredDuration = requiredDuration;
this.countTotalDuration = countTotalDuration;
this.durationInfoTextId = durationInfoTextId;
}
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalHasTimeLimit : Modifier
{
private readonly float timeLimit;
private readonly string timeLimitInfoTextId;
public override IEnumerable<string> InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[timelimit]" });
public override IEnumerable<string> InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{TimeSpan.FromSeconds(timeLimit):g}" });
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
{
var infoText = base.GetInfoText(traitor, textId, keys, values);
return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, new[] { "[infotext]", "[timelimit]" }, new[] { infoText, $"{TimeSpan.FromSeconds(timeLimit):g}" }) : infoText;
}
public override bool CanBeCompleted(ICollection<Traitor> traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f);
private float timeRemaining;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
timeRemaining = System.Math.Max(0.0f, timeRemaining - deltaTime);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
timeRemaining = timeLimit;
return true;
}
public GoalHasTimeLimit(Goal goal, float timeLimit, string timeLimitInfoTextId) : base(goal)
{
this.timeLimit = timeLimit;
this.timeLimitInfoTextId = timeLimitInfoTextId;
}
}
}
}
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public sealed class GoalIsOptional : Modifier
{
private readonly string optionalInfoTextId;
public override string StatusValueTextId => (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors)) ? "failed" : base.StatusValueTextId;
public override IEnumerable<string> StatusTextValues(Traitor traitor)
{
var values = base.StatusTextValues(traitor).ToArray();
values[1] = TextManager.GetServerMessage(StatusValueTextId);
return values;
}
public override bool IsCompleted => base.IsCompleted || (Traitors.Any(IsStarted) && !base.CanBeCompleted(Traitors));
public override bool CanBeCompleted(ICollection<Traitor> traitors) => true;
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values)
{
var infoText = base.GetInfoText(traitor, textId, keys, values);
return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, new[] { "[infotext]" }, new[] { infoText }) : infoText;
}
public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal)
{
this.optionalInfoTextId = optionalInfoTextId;
}
}
}
}
@@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public abstract class Modifier : Goal
{
protected Goal Goal { get; }
public override string StatusValueTextId => Goal.StatusValueTextId;
public override string StatusTextId
{
get => Goal.StatusTextId;
set => Goal.StatusTextId = value;
}
public override string InfoTextId
{
get => Goal.InfoTextId;
set => Goal.InfoTextId = value;
}
public override string CompletedTextId
{
get => Goal.CompletedTextId;
set => Goal.CompletedTextId = value;
}
public override IEnumerable<string> StatusTextKeys => Goal.StatusTextKeys;
public override IEnumerable<string> StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) };
public override IEnumerable<string> InfoTextKeys => Goal.InfoTextKeys;
public override IEnumerable<string> InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor);
public override IEnumerable<string> CompletedTextKeys => Goal.CompletedTextKeys;
public override IEnumerable<string> CompletedTextValues(Traitor traitor) => Goal.CompletedTextValues(traitor);
protected internal override string GetStatusText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => Goal.GetStatusText(traitor, textId, keys, values);
protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => Goal.GetInfoText(traitor, textId, keys, values);
protected internal override string GetCompletedText(Traitor traitor, string textId, IEnumerable<string> keys, IEnumerable<string> values) => Goal.GetCompletedText(traitor, textId, keys, values);
public override string StatusText(Traitor traitor) => GetStatusText(traitor, StatusTextId, StatusTextKeys, StatusTextValues(traitor));
public override string InfoText(Traitor traitor) => GetInfoText(traitor, InfoTextId, InfoTextKeys, InfoTextValues(traitor));
public override string CompletedText(Traitor traitor) => CompletedTextId != null ? GetCompletedText(traitor, CompletedTextId, CompletedTextKeys, CompletedTextValues(traitor)) : StatusText(traitor);
public override bool IsCompleted => Goal.IsCompleted;
public override bool IsStarted(Traitor traitor) => base.IsStarted(traitor) && Goal.IsStarted(traitor);
public override bool CanBeCompleted(ICollection<Traitor> traitors) => base.CanBeCompleted(traitors) && Goal.CanBeCompleted(traitors);
public override bool IsEnemy(Character character) => base.IsEnemy(character) || Goal.IsEnemy(character);
public override void Update(float deltaTime)
{
base.Update(deltaTime);
Goal.Update(deltaTime);
}
public override bool Start(Traitor traitor)
{
if (!base.Start(traitor))
{
return false;
}
if (!Goal.Start(traitor))
{
return false;
}
return true;
}
protected Modifier(Goal goal) : base()
{
Goal = goal;
}
}
}
}
@@ -0,0 +1,193 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class Traitor
{
public class Objective
{
public Traitor Traitor { get; private set; }
private int shuffleGoalsCount;
private readonly List<Goal> allGoals = new List<Goal>();
private readonly List<Goal> activeGoals = new List<Goal>();
private readonly List<Goal> pendingGoals = new List<Goal>();
private readonly List<Goal> completedGoals = new List<Goal>();
public bool IsCompleted => pendingGoals.Count <= 0;
public bool IsPartiallyCompleted => completedGoals.Count > 0;
public bool IsStarted { get; private set; } = false;
public bool CanBeStarted(ICollection<Traitor> traitors) => !IsStarted && allGoals.Any(goal => goal.CanBeCompleted(traitors));
public bool CanBeCompleted => !IsStarted || pendingGoals.All(goal => goal.CanBeCompleted(goal.Traitors));
public bool IsEnemy(Character character) => pendingGoals.Any(goal => goal.IsEnemy(character));
public bool IsAllowedToDamage(Structure structure) => pendingGoals.Any(goal => goal.IsAllowedToDamage(structure));
public readonly HashSet<string> Roles = new HashSet<string>();
public string InfoText { get; private set; }
public virtual string GoalInfoFormatId { get; set; } = "TraitorObjectiveGoalInfoFormat";
public string GoalInfos =>
string.Join("/",
string.Join("/", activeGoals.Select((goal, index) =>
{
var statusText = goal.StatusText(Traitor);
var startIndex = statusText.LastIndexOf('/') + 1;
return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}";
}).ToArray()),
string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray()));
public string AllGoalInfos =>
string.Join("/",
string.Join("/", allGoals.Select((goal, index) =>
{
var statusText = goal.StatusText(Traitor);
var startIndex = statusText.LastIndexOf('/') + 1;
return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}";
}).ToArray()),
string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray()));
public virtual string StartMessageTextId { get; set; } = "TraitorObjectiveStartMessage";
public virtual IEnumerable<string> StartMessageKeys => new string[] { "[traitorgoalinfos]" };
public virtual IEnumerable<string> StartMessageValues => new string[] { GoalInfos };
public virtual string StartMessageText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageTextId, StartMessageKeys, StartMessageValues);
public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer";
public virtual IEnumerable<string> StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" });
public virtual IEnumerable<string> StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" });
public virtual string StartMessageServerText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageServerTextId, StartMessageServerKeys, StartMessageServerValues);
public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess";
public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead";
public virtual string EndMessageSuccessDetainedTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDetained";
public virtual string EndMessageFailureTextId { get; set; } = "TraitorObjectiveEndMessageFailure";
public virtual string EndMessageFailureDeadTextId { get; set; } = "TraitorObjectiveEndMessageFailureDead";
public virtual string EndMessageFailureDetainedTextId { get; set; } = "TraitorObjectiveEndMessageFailureDetained";
public virtual IEnumerable<string> EndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" };
public virtual IEnumerable<string> EndMessageValues => new string[] { Traitor?.Character?.Name ?? "(unknown)", GoalInfos };
public virtual string EndMessageText
{
get
{
var traitorIsDead = Traitor.Character.IsDead;
var traitorIsDetained = Traitor.Character.LockHands;
var messageId = IsCompleted
? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId)
: (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId);
return TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, messageId, EndMessageKeys.ToArray(), EndMessageValues.ToArray());
}
}
public bool Start(Traitor traitor)
{
Traitor = traitor;
activeGoals.Clear();
pendingGoals.Clear();
completedGoals.Clear();
var allGoalsCount = allGoals.Count;
var indices = allGoals.Select((goal, index) => index).ToArray();
if (shuffleGoalsCount > 0)
{
for (var i = allGoalsCount; i > 1;)
{
int j = TraitorManager.RandomInt(i--);
var temp = indices[j];
indices[j] = indices[i];
indices[i] = temp;
}
}
for (var i = 0; i < allGoalsCount; ++i)
{
var goal = allGoals[indices[i]];
if (goal.Start(traitor))
{
activeGoals.Add(goal);
pendingGoals.Add(goal);
if (shuffleGoalsCount > 0 && pendingGoals.Count >= shuffleGoalsCount)
{
break;
}
}
else
{
completedGoals.Add(goal);
}
}
if (pendingGoals.Count <= 0 && completedGoals.Count < allGoals.Count)
{
return false;
}
IsStarted = true;
traitor.SendChatMessageBox(StartMessageText, traitor.Mission?.Identifier);
traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission?.Identifier);
return true;
}
public void StartMessage()
{
Traitor.SendChatMessage(StartMessageText, Traitor.Mission?.Identifier);
}
public void EndMessage()
{
Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission?.Identifier);
Traitor.SendChatMessage(EndMessageText, Traitor.Mission?.Identifier);
}
public void Update(float deltaTime)
{
if (!IsStarted)
{
return;
}
for (int i = 0; i < pendingGoals.Count;)
{
var goal = pendingGoals[i];
goal.Update(deltaTime);
if (!goal.IsCompleted)
{
++i;
}
else
{
completedGoals.Add(goal);
pendingGoals.RemoveAt(i);
if (GameMain.Server != null)
{
Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission?.Identifier);
if (pendingGoals.Count > 0)
{
Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission?.Identifier);
}
Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission?.Identifier);
}
}
}
}
public Objective(string infoText, int shuffleGoalsCount, ICollection<string> roles, ICollection<Goal> goals)
{
InfoText = infoText;
this.shuffleGoalsCount = shuffleGoalsCount;
Roles.UnionWith(roles);
allGoals.AddRange(goals);
}
}
}
}
@@ -0,0 +1,58 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class Traitor
{
public readonly Character Character;
public string Role { get; }
public TraitorMission Mission { get; }
public Objective CurrentObjective => Mission.GetCurrentObjective(this);
public Traitor(TraitorMission mission, string role, Character character)
{
Mission = mission;
Role = role;
Character = character;
Character.IsTraitor = true;
GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.Status });
}
public delegate void MessageSender(string message);
public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender)
{
string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, new string[] {
"[codewords]", "[coderesponse]"
}, new string[] {
codeWords, codeResponse
});
messageSender(greetingMessage);
Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character);
Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection);
if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null)
{
GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText, Mission?.Identifier, TraitorMessageType.ServerMessageBox);
}
}
public void SendChatMessage(string serverText, string iconIdentifier)
{
Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character);
GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server);
}
public void SendChatMessageBox(string serverText, string iconIdentifier)
{
Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character);
GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox);
}
public void UpdateCurrentObjective(string objectiveText, string iconIdentifier)
{
Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character);
Character.TraitorCurrentObjective = objectiveText;
GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective, iconIdentifier, TraitorMessageType.Objective);
}
}
}
@@ -0,0 +1,195 @@
// #define DISABLE_MISSIONS
using System;
using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class TraitorManager
{
public static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks);
// All traitor related functionality should use the following interface for generating random values
public static int RandomInt(int n) => Random.Next(n);
// All traitor related functionality should use the following interface for generating random values
public static double RandomDouble() => Random.NextDouble();
public readonly Dictionary<Character.TeamType, Traitor.TraitorMission> Missions = new Dictionary<Character.TeamType, Traitor.TraitorMission>();
public string GetCodeWords(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : "";
public string GetCodeResponse(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : "";
public IEnumerable<Traitor> Traitors => Missions.Values.SelectMany(mission => mission.Traitors.Values);
private float startCountdown = 0.0f;
private GameServer server;
public bool ShouldEndRound
{
get;
set;
}
public bool IsTraitor(Character character)
{
if (Traitors == null)
{
return false;
}
return Traitors.Any(traitor => traitor.Character == character);
}
public string GetTraitorRole(Character character)
{
var traitor = Traitors.FirstOrDefault(candidate => candidate.Character == character);
if (traitor == null)
{
return "";
}
return traitor.Role;
}
public TraitorManager()
{
}
public void Start(GameServer server)
{
#if DISABLE_MISSIONS
return;
#endif
if (server == null) { return; }
ShouldEndRound = false;
this.server = server;
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)RandomDouble());
}
public void SkipStartDelay()
{
startCountdown = 0.01f;
}
public void Update(float deltaTime)
{
if (ShouldEndRound) { return; }
#if DISABLE_MISSIONS
return;
#endif
if (Missions.Any())
{
bool missionCompleted = false;
bool gameShouldEnd = false;
Character.TeamType winningTeam = Character.TeamType.None;
foreach (var mission in Missions)
{
mission.Value.Update(deltaTime, () =>
{
switch (mission.Key)
{
case Character.TeamType.Team1:
winningTeam = (winningTeam == Character.TeamType.None) ? Character.TeamType.Team2 : Character.TeamType.None;
break;
case Character.TeamType.Team2:
winningTeam = (winningTeam == Character.TeamType.None) ? Character.TeamType.Team1 : Character.TeamType.None;
break;
default:
break;
}
gameShouldEnd = true;
});
if (!gameShouldEnd && mission.Value.IsCompleted)
{
missionCompleted = true;
foreach (var traitor in mission.Value.Traitors.Values)
{
traitor.UpdateCurrentObjective("", mission.Value.Identifier);
}
}
}
if (gameShouldEnd)
{
GameMain.GameSession.WinningTeam = winningTeam;
ShouldEndRound = true;
return;
}
if (missionCompleted)
{
Missions.Clear();
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble());
}
}
else if (startCountdown > 0.0f && server.GameStarted)
{
startCountdown -= deltaTime;
if (startCountdown <= 0.0f)
{
int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0);
if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount)
{
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble());
return;
}
if (GameMain.GameSession.Mission is CombatMission)
{
var teamIds = new[] { Character.TeamType.Team1, Character.TeamType.Team2 };
foreach (var teamId in teamIds)
{
if (server.ConnectedClients.Count(c => c.Character != null && !c.Character.IsDead && c.TeamID == teamId) < 2)
{
continue;
}
var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate();
if (mission != null)
{
Missions.Add(teamId, mission);
}
}
var canBeStartedCount = Missions.Sum(mission => mission.Value.CanBeStarted(server, this, mission.Key) ? 1 : 0);
if (canBeStartedCount >= Missions.Count)
{
var startSuccessCount = Missions.Sum(mission => mission.Value.Start(server, this, mission.Key) ? 1 : 0);
if (startSuccessCount >= Missions.Count)
{
return;
}
}
}
else
{
var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate();
if (mission != null) {
if (mission.CanBeStarted(server, this, Character.TeamType.None))
{
if (mission.Start(server, this, Character.TeamType.None))
{
Missions.Add(Character.TeamType.None, mission);
return;
}
}
}
}
Missions.Clear();
startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble());
}
}
}
public string GetEndMessage()
{
#if DISABLE_MISSIONS
return "";
#endif
if (GameMain.Server == null || !Missions.Any()) return "";
return TextManager.JoinServerMessages("\n\n", Missions.Select(mission => mission.Value.GlobalEndMessage).ToArray());
}
}
}
@@ -0,0 +1,401 @@
//#define ALLOW_SOLO_TRAITOR
//#define ALLOW_NONHUMANOID_TRAITOR
using System;
using Barotrauma.Networking;
using Lidgren.Network;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Barotrauma.Extensions;
namespace Barotrauma
{
partial class Traitor
{
public class TraitorMission
{
private static string wordsTxt = Path.Combine("Content", "CodeWords.txt");
private readonly List<Objective> allObjectives = new List<Objective>();
private readonly List<Objective> pendingObjectives = new List<Objective>();
private readonly List<Objective> completedObjectives = new List<Objective>();
/// <summary>
/// Has the mission been completed (does not mean that the traitor necessarily won, the mission is considered completed if the traitor fails for whatever reason)
/// </summary>
public bool IsCompleted => pendingObjectives.Count <= 0;
public readonly Dictionary<string, Traitor> Traitors = new Dictionary<string, Traitor>();
public delegate bool RoleFilter(Character character);
public readonly Dictionary<string, RoleFilter> Roles = new Dictionary<string, RoleFilter>();
public string StartText { get; private set; }
public string CodeWords { get; private set; }
public string CodeResponse { get; private set; }
public string GlobalEndMessageSuccessTextId { get; private set; }
public string GlobalEndMessageSuccessDeadTextId { get; private set; }
public string GlobalEndMessageSuccessDetainedTextId { get; private set; }
public string GlobalEndMessageFailureTextId { get; private set; }
public string GlobalEndMessageFailureDeadTextId { get; private set; }
public string GlobalEndMessageFailureDetainedTextId { get; private set; }
public readonly string Identifier;
public virtual IEnumerable<string> GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" };
public virtual IEnumerable<string> GlobalEndMessageValues {
get {
var isSuccess = completedObjectives.Count >= allObjectives.Count;
return new string[] {
string.Join(", ", Traitors.Values.Select(traitor => traitor.Character?.Name ?? "(unknown)")),
(isSuccess ? completedObjectives.LastOrDefault() : pendingObjectives.FirstOrDefault())?.GoalInfos ?? ""
};
}
}
public string GlobalEndMessage
{
get
{
if (Traitors.Any() && allObjectives.Count > 0)
{
return TextManager.JoinServerMessages("\n",
Traitors.Values.Select(traitor =>
{
var isSuccess = completedObjectives.Count >= allObjectives.Count;
var traitorIsDead = traitor.Character.IsDead;
var traitorIsDetained = traitor.Character.LockHands;
var messageId = isSuccess
? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId)
: (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId);
return TextManager.FormatServerMessageWithGenderPronouns(traitor.Character?.Info?.Gender ?? Gender.None, messageId, GlobalEndMessageKeys.ToArray(), GlobalEndMessageValues.ToArray());
}).ToArray());
}
return "";
}
}
public Objective GetCurrentObjective(Traitor traitor)
{
if (!Traitors.ContainsValue(traitor) || pendingObjectives.Count <= 0)
{
return null;
}
return pendingObjectives.Find(objective => objective.Roles.Contains(traitor.Role));
}
protected List<Tuple<Client, Character>> FindTraitorCandidates(GameServer server, Character.TeamType team, RoleFilter traitorRoleFilter)
{
var traitorCandidates = new List<Tuple<Client, Character>>();
foreach (Client c in server.ConnectedClients)
{
if (c.Character == null || c.Character.IsDead || c.Character.Removed || !traitorRoleFilter(c.Character) ||
(team != Character.TeamType.None && c.Character.TeamID != team))
{
continue;
}
#if !ALLOW_NONHUMANOID_TRAITOR
if (!c.Character.IsHumanoid) { continue; }
#endif
traitorCandidates.Add(Tuple.Create(c, c.Character));
}
return traitorCandidates;
}
protected List<Character> FindCharacters()
{
List<Character> characters = new List<Character>();
foreach (var character in Character.CharacterList)
{
characters.Add(character);
}
return characters;
}
protected List<Tuple<string, Tuple<Client, Character>>> AssignTraitors(GameServer server, TraitorManager traitorManager, Character.TeamType team)
{
List<Character> characters = FindCharacters();
#if !ALLOW_SOLO_TRAITOR
if (characters.Count < 2)
{
return null;
}
#endif
var roleCandidates = new Dictionary<string, HashSet<Tuple<Client, Character>>>();
foreach (var role in Roles)
{
roleCandidates.Add(role.Key, new HashSet<Tuple<Client, Character>>(FindTraitorCandidates(server, team, role.Value)));
if (roleCandidates[role.Key].Count <= 0)
{
return null;
}
}
var candidateRoleCounts = new Dictionary<Tuple<Client, Character>, int>();
foreach (var candidateEntry in roleCandidates)
{
foreach (var candidate in candidateEntry.Value)
{
candidateRoleCounts[candidate] = candidateRoleCounts.TryGetValue(candidate, out var count) ? count + 1 : 1;
}
}
var unassignedRoles = new List<string>(roleCandidates.Keys);
unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count);
var assignedCandidates = new List<Tuple<string, Tuple<Client, Character>>>();
while (unassignedRoles.Count > 0)
{
var currentRole = unassignedRoles[0];
var availableCandidates = roleCandidates[currentRole].ToList();
if (availableCandidates.Count <= 0)
{
break;
}
unassignedRoles.RemoveAt(0);
availableCandidates.Sort((a, b) => candidateRoleCounts[b] - candidateRoleCounts[a]);
unassignedRoles.Sort((a, b) => roleCandidates[a].Count - roleCandidates[b].Count);
int numCandidates = 1;
for (int i = 1; i < availableCandidates.Count && candidateRoleCounts[availableCandidates[i]] == candidateRoleCounts[availableCandidates[0]]; ++i)
{
++numCandidates;
}
var selected = ToolBox.SelectWeightedRandom(availableCandidates, availableCandidates.Select(c => Math.Max(c.Item1.RoundsSincePlayedAsTraitor, 0.1f)).ToList(), TraitorManager.Random);
assignedCandidates.Add(Tuple.Create(currentRole, selected));
foreach (var candidate in roleCandidates.Values)
{
candidate.Remove(selected);
}
}
if (unassignedRoles.Count > 0)
{
return null;
}
return assignedCandidates;
}
public bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team)
{
foreach (var role in Roles)
{
var candidates = FindTraitorCandidates(server, team, role.Value);
if (candidates.Count <= 0)
{
return false;
}
}
return AssignTraitors(server, traitorManager, team) != null;
}
public bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team)
{
var assignedCandidates = AssignTraitors(server, traitorManager, team);
if (assignedCandidates == null)
{
return false;
}
foreach (Client client in server.ConnectedClients)
{
client.RoundsSincePlayedAsTraitor++;
}
Traitors.Clear();
foreach (var candidate in assignedCandidates)
{
var traitor = new Traitor(this, candidate.Item1, candidate.Item2.Item1.Character);
Traitors.Add(candidate.Item1, traitor);
candidate.Item2.Item1.RoundsSincePlayedAsTraitor = 0;
}
CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt);
CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt);
if (pendingObjectives.Count <= 0 || !pendingObjectives[0].CanBeStarted(Traitors.Values))
{
Traitors.Clear();
return false;
}
var pendingMessages = new Dictionary<Traitor, List<string>>();
pendingMessages.Clear();
foreach (var traitor in Traitors.Values)
{
pendingMessages.Add(traitor, new List<string>());
}
foreach (var traitor in Traitors.Values)
{
traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message));
}
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier)));
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier)));
Update(0.0f, () => { GameMain.Server.TraitorManager.ShouldEndRound = true; });
#if SERVER
foreach (var traitor in Traitors.Values)
{
GameServer.Log($"{traitor.Character.Name} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage);
}
#endif
return true;
}
public delegate void TraitorWinHandler();
public void Update(float deltaTime, TraitorWinHandler winHandler)
{
if (pendingObjectives.Count <= 0 || Traitors.Count <= 0)
{
return;
}
if (Traitors.Values.Any(traitor => traitor.Character?.IsDead ?? true || traitor.Character.Removed))
{
Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier));
pendingObjectives.Clear();
Traitors.Clear();
return;
}
var startedObjectives = new List<Objective>();
foreach (var traitor in Traitors.Values)
{
startedObjectives.Clear();
while (pendingObjectives.Count > 0)
{
var objective = GetCurrentObjective(traitor);
if (objective == null)
{
// No more objectives left for traitor or waiting for another traitor's objective.
break;
}
if (!objective.IsStarted)
{
if (!objective.Start(traitor))
{
//the mission fails if an objective cannot be started
if (completedObjectives.Count > 0)
{
objective.EndMessage();
}
pendingObjectives.Clear();
break;
}
startedObjectives.Add(objective);
}
objective.Update(deltaTime);
if (objective.IsCompleted)
{
pendingObjectives.Remove(objective);
completedObjectives.Add(objective);
if (pendingObjectives.Count > 0)
{
objective.EndMessage();
}
continue;
}
if (objective.IsStarted && !objective.CanBeCompleted)
{
objective.EndMessage();
pendingObjectives.Clear();
}
break;
}
if (pendingObjectives.Count > 0)
{
startedObjectives.ForEach(objective => objective.StartMessage());
}
}
if (completedObjectives.Count >= allObjectives.Count)
{
foreach (var traitor in Traitors)
{
SteamAchievementManager.OnTraitorWin(traitor.Value.Character);
}
winHandler();
}
}
public delegate bool CharacterFilter(Character character);
public List<Character> FindKillTarget(Character traitor, CharacterFilter filter, int count = -1, float percentage = -1f)
{
if (traitor == null) { return null; }
List<Character> validCharacters = Character.CharacterList.FindAll(c => c.TeamID == traitor.TeamID &&
c != traitor && !c.IsDead &&
(filter == null || filter(c)));
int targetCount = 1;
if (count > 0)
{
targetCount = count;
}
else if (percentage > 0f)
{
targetCount = (int)Math.Max(1, Math.Floor(validCharacters.Count * percentage));
}
List<Character> targetCharacters = new List<Character>();
if (validCharacters.Count > 0)
{
for (int i = 0; i < targetCount; i++)
{
if (validCharacters.Count == 0) break;
Character character = validCharacters[TraitorManager.RandomInt(validCharacters.Count)];
targetCharacters.Add(character);
validCharacters.Remove(character);
}
return targetCharacters;
}
#if ALLOW_SOLO_TRAITOR
targetCharacters.Add(traitor);
return targetCharacters;
#else
return null;
#endif
}
public string GetTargetNames(List<Character> targets)
{
string names = string.Empty;
for (int i = 0; i < targets.Count; i++)
{
names += targets[i].Name;
if (i < targets.Count - 1)
{
names += ", ";
}
}
if (names.Length > 0)
{
return names;
}
else
{
return TextManager.FormatServerMessage("unknown");
}
}
public TraitorMission(string identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable<KeyValuePair<string, RoleFilter>> roles, ICollection<Objective> objectives)
{
Identifier = identifier;
StartText = startText;
GlobalEndMessageSuccessTextId = globalEndMessageSuccessTextId;
GlobalEndMessageSuccessDeadTextId = globalEndMessageSuccessDeadTextId;
GlobalEndMessageSuccessDetainedTextId = globalEndMessageSuccessDetainedTextId;
GlobalEndMessageFailureTextId = globalEndMessageFailureTextId;
GlobalEndMessageFailureDeadTextId = globalEndMessageFailureDeadTextId;
GlobalEndMessageFailureDetainedTextId = globalEndMessageFailureDetainedTextId;
foreach (var role in roles)
{
Roles.Add(role.Key, role.Value);
}
allObjectives.AddRange(objectives);
pendingObjectives.AddRange(objectives);
}
}
}
}
@@ -0,0 +1,658 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
class TraitorMissionPrefab
{
public class TraitorMissionEntry
{
public readonly TraitorMissionPrefab Prefab;
public float SelectedWeight;
public TraitorMissionEntry(XElement element)
{
Prefab = new TraitorMissionPrefab(element);
}
}
public static readonly List<TraitorMissionEntry> List = new List<TraitorMissionEntry>();
public static void Init()
{
var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions);
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc?.Root == null) continue;
foreach (XElement element in doc.Root.Elements())
{
List.Add(new TraitorMissionEntry(element));
}
}
}
public static TraitorMissionPrefab RandomPrefab()
{
var selected = ToolBox.SelectWeightedRandom(List, List.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random);
//the weight of the missions that didn't get selected keeps growing the make them more likely to get picked
foreach (var mission in List)
{
mission.SelectedWeight += 10;
}
selected.SelectedWeight = 0.0f;
return selected.Prefab;
}
private class AttributeChecker : IDisposable
{
private readonly XElement element;
private readonly HashSet<string> required = new HashSet<string>();
private readonly HashSet<string> optional = new HashSet<string>();
public void Optional(params string[] names)
{
optional.UnionWith(names);
}
public void Required(params string[] names)
{
required.UnionWith(names);
}
public void Dispose()
{
foreach (var requiredName in required)
{
if (element.Attributes().All(attribute => attribute.Name != requiredName))
{
GameServer.Log($"Required attribute \"{requiredName}\" is missing in \"{element.Name}\"", ServerLog.MessageType.Error);
}
}
foreach (var attribute in element.Attributes())
{
var attributeName = attribute.Name.ToString();
if (!required.Contains(attributeName) && !optional.Contains(attributeName))
{
GameServer.Log($"Unsupported attribute \"{attributeName}\" in \"{element.Name}\"", ServerLog.MessageType.Error);
}
}
}
public AttributeChecker(XElement element)
{
this.element = element;
}
}
public class Goal
{
public readonly string Type;
public readonly XElement Config;
public Goal(string type, XElement config)
{
Type = type;
Config = config;
}
private delegate bool TargetFilter(string value, Character character);
private static Dictionary<string, TargetFilter> targetFilters = new Dictionary<string, TargetFilter>()
{
{ "job", (value, character) => value.Equals(character.Info.Job.Prefab.Identifier, StringComparison.OrdinalIgnoreCase) },
{ "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) }
};
public Traitor.Goal Instantiate()
{
Traitor.Goal goal = null;
using (var checker = new AttributeChecker(Config))
{
checker.Required("type");
var goalType = Config.GetAttributeString("type", "");
switch (goalType.ToLowerInvariant())
{
case "killtarget":
{
checker.Optional(targetFilters.Keys.ToArray());
checker.Optional("causeofdeath");
checker.Optional("affliction");
checker.Optional("roomname");
checker.Optional("targetcount");
checker.Optional("targetpercentage");
List<Traitor.TraitorMission.CharacterFilter> killFilters = new List<Traitor.TraitorMission.CharacterFilter>();
foreach (var attribute in Config.Attributes())
{
if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
{
killFilters.Add((character) => filter(attribute.Value, character));
}
}
goal = new Traitor.GoalKillTarget((character) => killFilters.All(f => f(character)),
(CauseOfDeathType)Enum.Parse(typeof(CauseOfDeathType), Config.GetAttributeString("causeofdeath", "Unknown"), true),
Config.GetAttributeString("affliction", null), Config.GetAttributeString("targethull", null), Config.GetAttributeInt("targetcount", -1),
Config.GetAttributeFloat("targetpercentage", -1f));
break;
}
case "destroyitems":
{
checker.Required("tag");
checker.Optional("percentage", "matchIdentifier", "matchTag", "matchInventory");
var tag = Config.GetAttributeString("tag", null);
if (tag != null)
{
goal = new Traitor.GoalDestroyItemsWithTag(
tag,
Config.GetAttributeFloat("percentage", 100.0f) / 100.0f,
Config.GetAttributeBool("matchIdentifier", true),
Config.GetAttributeBool("matchTag", true),
Config.GetAttributeBool("matchInventory", false));
}
break;
}
case "sabotage":
{
checker.Required("tag");
checker.Optional("threshold");
var tag = Config.GetAttributeString("tag", null);
if (tag != null)
{
goal = new Traitor.GoalSabotageItems(tag, Config.GetAttributeFloat("threshold", 20.0f));
}
break;
}
case "floodsub":
checker.Optional("percentage");
goal = new Traitor.GoalFloodPercentOfSub(Config.GetAttributeFloat("percentage", 100.0f) / 100.0f);
break;
case "finditem":
checker.Required("identifier");
checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers", "percentage");
List<Traitor.TraitorMission.CharacterFilter> itemCountFilters = new List<Traitor.TraitorMission.CharacterFilter>();
foreach (var attribute in Config.Attributes())
{
if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
{
itemCountFilters.Add((character) => filter(attribute.Value, character));
}
}
goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), Config.GetAttributeString("identifier", null), Config.GetAttributeBool("preferNew", true), Config.GetAttributeBool("allowNew", true), Config.GetAttributeBool("allowExisting", true), Config.GetAttributeFloat("percentage", -1f), Config.GetAttributeStringArray("allowedContainers", new string[] {"steelcabinet", "mediumsteelcabinet", "suppliescabinet"}));
break;
case "replaceinventory":
checker.Required("containers", "replacements");
checker.Optional("percentage");
goal = new Traitor.GoalReplaceInventory(Config.GetAttributeStringArray("containers", new string[] { }), Config.GetAttributeStringArray("replacements", new string[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f);
break;
case "reachdistancefromsub":
checker.Optional("distance");
goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 125f));
break;
case "injectpoison":
checker.Optional(targetFilters.Keys.ToArray());
checker.Required("poison");
checker.Required("affliction");
checker.Optional("targetcount");
checker.Optional("targetpercentage");
List<Traitor.TraitorMission.CharacterFilter> poisonFilters = new List<Traitor.TraitorMission.CharacterFilter>();
foreach (var attribute in Config.Attributes())
{
if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
{
poisonFilters.Add((character) => filter(attribute.Value, character));
}
}
goal = new Traitor.GoalInjectTarget((character) => poisonFilters.All(f => f(character)), Config.GetAttributeString("poison", null),
Config.GetAttributeString("affliction", null), Config.GetAttributeInt("targetcount", -1), Config.GetAttributeFloat("targetpercentage", -1f));
break;
case "unwire":
checker.Required("tag");
checker.Optional("connectionname");
checker.Optional("connectiondisplayname");
goal = new Traitor.GoalUnwiring(Config.GetAttributeString("tag", null), Config.GetAttributeString("connectionname", null), Config.GetAttributeString("connectiondisplayname)", null));
break;
case "transformentity":
checker.Required("entities", "entitytypes");
checker.Optional("catalystid");
goal = new Traitor.GoalEntityTransformation(Config.GetAttributeStringArray("entities", null), Config.GetAttributeStringArray("entitytypes", null), Config.GetAttributeString("catalystid", null));
break;
case "keeptransformedalive":
checker.Required("speciesname");
goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeString("speciesname", null));
break;
default:
GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error);
break;
}
}
if (goal == null)
{
return null;
}
foreach (var element in Config.Elements())
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "modifier":
{
using (var checker = new AttributeChecker(element))
{
checker.Required("type");
var modifierType = element.GetAttributeString("type", "");
switch (modifierType)
{
case "duration":
{
checker.Optional("cumulative", "duration", "infotext");
var isCumulative = element.GetAttributeBool("cumulative", false);
goal = new Traitor.GoalHasDuration(goal, element.GetAttributeFloat("duration", 5.0f), isCumulative, element.GetAttributeString("infotext", isCumulative ? "TraitorGoalWithCumulativeDurationInfoText" : "TraitorGoalWithDurationInfoText"));
break;
}
case "timelimit":
checker.Optional("timelimit", "infotext");
goal = new Traitor.GoalHasTimeLimit(goal, element.GetAttributeFloat("timelimit", 180.0f), element.GetAttributeString("infotext", "TraitorGoalWithTimeLimitInfoText"));
break;
case "optional":
checker.Optional("infotext");
goal = new Traitor.GoalIsOptional(goal, element.GetAttributeString("infotext", "TraitorGoalIsOptionalInfoText"));
break;
default:
GameServer.Log($"Unrecognized modifier type \"{modifierType}\".", ServerLog.MessageType.Error);
break;
}
}
break;
}
}
}
foreach (var element in Config.Elements())
{
var elementName = element.Name.ToString().ToLowerInvariant();
switch (elementName)
{
case "modifier":
// loaded above
break;
case "infotext":
{
using (var checker = new AttributeChecker(element))
{
checker.Required("id");
var id = element.GetAttributeString("id", null);
if (id != null)
{
goal.InfoTextId = id;
}
}
break;
}
case "completedtext":
{
using (var checker = new AttributeChecker(element))
{
checker.Required("id");
var id = element.GetAttributeString("id", null);
if (id != null)
{
goal.CompletedTextId = id;
}
}
break;
}
default:
GameServer.Log($"Unrecognized element \"{element.Name}\" in goal.", ServerLog.MessageType.Error);
break;
}
}
return goal;
}
}
public abstract class ObjectiveBase
{
public HashSet<string> Roles { get; } = new HashSet<string>();
public abstract void InstantiateGoals();
public abstract Traitor.Objective Instantiate(IEnumerable<string> roles);
}
protected class Objective : ObjectiveBase
{
public string InfoText { get; internal set; }
public string StartMessageTextId { get; internal set; }
public string StartMessageServerTextId { get; internal set; }
public string EndMessageSuccessTextId { get; internal set; }
public string EndMessageSuccessDeadTextId { get; internal set; }
public string EndMessageSuccessDetainedTextId { get; internal set; }
public string EndMessageFailureTextId { get; internal set; }
public string EndMessageFailureDeadTextId { get; internal set; }
public string EndMessageFailureDetainedTextId { get; internal set; }
public int ShuffleGoalsCount { get; internal set; }
public readonly List<Goal> Goals = new List<Goal>();
private List<Traitor.Goal> goalInstances = null;
public override void InstantiateGoals()
{
goalInstances = Goals.ConvertAll(goal =>
{
var instance = goal.Instantiate();
if (instance == null)
{
GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error);
}
return instance;
}).FindAll(goal => goal != null);
}
public override Traitor.Objective Instantiate(IEnumerable<string> roles)
{
var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, roles.ToArray(), goalInstances);
if (StartMessageTextId != null)
{
result.StartMessageTextId = StartMessageTextId;
}
if (StartMessageServerTextId != null)
{
result.StartMessageServerTextId = StartMessageServerTextId;
}
if (EndMessageSuccessTextId != null)
{
result.EndMessageSuccessTextId = EndMessageSuccessTextId;
}
if (EndMessageSuccessDeadTextId != null)
{
result.EndMessageSuccessDeadTextId = EndMessageSuccessDeadTextId;
}
if (EndMessageSuccessDetainedTextId != null)
{
result.EndMessageSuccessDetainedTextId = EndMessageSuccessDetainedTextId;
}
if (EndMessageFailureTextId != null)
{
result.EndMessageFailureTextId = EndMessageFailureTextId;
}
if (EndMessageFailureDeadTextId != null)
{
result.EndMessageFailureDeadTextId = EndMessageFailureDeadTextId;
}
if (EndMessageFailureDetainedTextId != null)
{
result.EndMessageFailureDetainedTextId = EndMessageFailureDetainedTextId;
}
return result;
}
}
protected class WaitObjective : ObjectiveBase
{
private Traitor.GoalWaitForTraitors sharedGoal;
public override void InstantiateGoals()
{
sharedGoal = new Traitor.GoalWaitForTraitors(Roles.Count);
}
public override Traitor.Objective Instantiate(IEnumerable<string> roles)
{
return new Traitor.Objective("TraitorObjectiveInfoTextWaitForOtherTraitors", -1, roles.ToArray(), new[] { sharedGoal });
}
public WaitObjective(ICollection<string> roles)
{
Roles.UnionWith(roles);
}
}
public class Role
{
public readonly Traitor.TraitorMission.RoleFilter Filter;
public Role(IEnumerable<Traitor.TraitorMission.RoleFilter> filters)
{
Filter = character => filters.All(filter => filter(character));
}
public Role()
{
Filter = character => true;
}
}
public readonly Dictionary<string, Role> Roles = new Dictionary<string, Role>();
public readonly string Identifier;
public readonly string StartText;
public readonly string EndMessageSuccessText;
public readonly string EndMessageSuccessDeadText;
public readonly string EndMessageSuccessDetainedText;
public readonly string EndMessageFailureText;
public readonly string EndMessageFailureDeadText;
public readonly string EndMessageFailureDetainedText;
public readonly List<ObjectiveBase> Objectives = new List<ObjectiveBase>();
public Traitor.TraitorMission Instantiate()
{
var objectivesWithSync = new List<ObjectiveBase>();
var objectivesCount = Objectives.Count;
if (objectivesCount > 0)
{
var pendingRoles = new HashSet<string>();
var pendingCount = 1;
objectivesWithSync.Add(Objectives[0]);
pendingRoles.UnionWith(Objectives[0].Roles);
for (var i = 1; i < objectivesCount; ++i)
{
var objective = Objectives[i];
if (pendingRoles.IsSupersetOf(objective.Roles))
{
if (pendingCount > 1)
{
objectivesWithSync.Add(new WaitObjective(objective.Roles));
}
pendingRoles.Clear();
pendingCount = 0;
}
objectivesWithSync.Add(objective);
pendingRoles.UnionWith(objective.Roles);
++pendingCount;
}
if (pendingCount > 1 && pendingRoles.IsSubsetOf(Roles.Keys))
{
// TODO: If last objective includes only one traitor, other traitors will get the wrong end message.
objectivesWithSync.Add(new WaitObjective(Roles.Keys));
}
}
return new Traitor.TraitorMission(
Identifier,
StartText ?? "TraitorMissionStartMessage",
EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess",
EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead",
EndMessageSuccessDetainedText ?? "TraitorObjectiveEndMessageSuccessDetained",
EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure",
EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead",
EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained",
Roles.ToDictionary(kv => kv.Key, kv => kv.Value.Filter),
objectivesWithSync.SelectMany(objective =>
{
objective.InstantiateGoals();
return objective.Roles.Select(role => objective.Instantiate(new[] { role }));
}).ToArray());
}
protected Goal LoadGoal(XElement goalRoot)
{
var goalType = goalRoot.GetAttributeString("type", "");
return new Goal(goalType, goalRoot);
}
protected Objective LoadObjective(XElement objectiveRoot, string[] allRoles)
{
var allRolesSet = new HashSet<string>(allRoles);
var result = new Objective
{
ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1)
};
var objectiveRoles = objectiveRoot.GetAttributeStringArray("roles", allRoles);
if (!allRolesSet.IsSupersetOf(objectiveRoles))
{
var unrecognized = new HashSet<string>(objectiveRoles);
unrecognized.ExceptWith(allRoles);
GameServer.Log($"Undefined role(s) \"{string.Join(", ", unrecognized)}\" set for Objective.", ServerLog.MessageType.Error);
}
result.Roles.UnionWith(allRolesSet.Intersect(objectiveRoles));
foreach (var element in objectiveRoot.Elements())
{
using (var checker = new AttributeChecker(element))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "infotext":
checker.Required("id");
result.InfoText = element.GetAttributeString("id", null);
break;
case "startmessage":
checker.Required("id");
result.StartMessageTextId = element.GetAttributeString("id", null);
break;
case "startmessageserver":
checker.Required("id");
result.StartMessageServerTextId = element.GetAttributeString("id", null);
break;
case "endmessagesuccess":
checker.Required("id");
result.EndMessageSuccessTextId = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdead":
checker.Required("id");
result.EndMessageSuccessDeadTextId = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdetained":
checker.Required("id");
result.EndMessageSuccessDetainedTextId = element.GetAttributeString("id", null);
break;
case "endmessagefailure":
checker.Required("id");
result.EndMessageFailureTextId = element.GetAttributeString("id", null);
break;
case "endmessagefailuredead":
checker.Required("id");
result.EndMessageFailureDeadTextId = element.GetAttributeString("id", null);
break;
case "endmessagefailuredetained":
checker.Required("id");
result.EndMessageFailureDetainedTextId = element.GetAttributeString("id", null);
break;
case "goal":
{
var goal = LoadGoal(element);
if (goal != null)
{
result.Goals.Add(goal);
}
break;
}
default:
GameServer.Log($"Unrecognized element \"{element.Name}\" under Objective.", ServerLog.MessageType.Error);
break;
}
}
}
return result;
}
protected Role LoadRole(XElement roleRoot)
{
var filters = new List<Traitor.TraitorMission.RoleFilter>();
var jobs = roleRoot.GetAttributeStringArray("jobs", null);
if (jobs != null)
{
var jobsSet = new HashSet<string>(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture)));
filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower(CultureInfo.InvariantCulture)));
}
return new Role(filters);
}
public TraitorMissionPrefab(XElement missionRoot)
{
Identifier = missionRoot.GetAttributeString("identifier", null);
foreach (var element in missionRoot.Elements())
{
using (var checker = new AttributeChecker(element))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "role":
checker.Required("id");
checker.Optional("jobs");
Roles.Add(element.GetAttributeString("id", null), LoadRole(element));
break;
}
}
}
if (!Roles.Any())
{
Roles.Add("traitor", new Role());
}
foreach (var element in missionRoot.Elements())
{
using (var checker = new AttributeChecker(element))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "role":
// handled above
break;
case "startinfotext":
checker.Required("id");
StartText = element.GetAttributeString("id", null);
break;
case "endmessagesuccess":
checker.Required("id");
EndMessageSuccessText = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdead":
checker.Required("id");
EndMessageSuccessDeadText = element.GetAttributeString("id", null);
break;
case "endmessagesuccessdetained":
checker.Required("id");
EndMessageSuccessDetainedText = element.GetAttributeString("id", null);
break;
case "endmessagefailure":
checker.Required("id");
EndMessageFailureText = element.GetAttributeString("id", null);
break;
case "endmessagefailuredead":
checker.Required("id");
EndMessageFailureDeadText = element.GetAttributeString("id", null);
break;
case "endmessagefailuredetained":
checker.Required("id");
EndMessageFailureDetainedText = element.GetAttributeString("id", null);
break;
case "objective":
{
var objective = LoadObjective(element, Roles.Keys.ToArray());
if (objective != null)
{
Objectives.Add(objective);
}
break;
}
default:
GameServer.Log($"Unrecognized element \"{element.Name}\"under TraitorMission.", ServerLog.MessageType.Error);
break;
}
}
}
}
}
}
@@ -0,0 +1,84 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
public static class XnaToConsoleColor
{
static Dictionary<Color, ConsoleColor> dictionary;
public static ConsoleColor Convert(Color xnaCol)
{
if (dictionary == null)
{
dictionary = new Dictionary<Color, ConsoleColor>
{
{ Color.White, ConsoleColor.White },
{ Color.Gray, ConsoleColor.Gray },
{ Color.LightGray, ConsoleColor.Gray },
{ Color.DarkGray, ConsoleColor.Gray },
{ Color.Red, ConsoleColor.Red },
{ Color.DarkRed, ConsoleColor.DarkRed },
{ Color.Yellow, ConsoleColor.Yellow },
{ Color.Orange, ConsoleColor.Yellow },
{ Color.Green, ConsoleColor.Green },
{ Color.Lime, ConsoleColor.Green },
{ Color.Blue, ConsoleColor.Blue },
{ Color.Cyan, ConsoleColor.Cyan },
{ Color.DarkBlue, ConsoleColor.DarkBlue },
{ Color.Pink, ConsoleColor.Magenta },
{ Color.Magenta, ConsoleColor.Magenta }
};
}
ConsoleColor val = ConsoleColor.White;
if (dictionary.TryGetValue(xnaCol, out val))
{
return val;
}
return GetClosestConsoleColor(xnaCol);
}
public static ConsoleColor GetClosestConsoleColor(Color color)
{
Vector3 hls = ToolBox.RgbToHLS(color.ToVector3());
if (hls.Z < 0.5)
{
// we have a grayish color
switch ((int)(hls.Y * 3.5))
{
case 0: return ConsoleColor.Black;
case 1: return ConsoleColor.DarkGray;
case 2: return ConsoleColor.Gray;
default: return ConsoleColor.White;
}
}
int hue = (int)Math.Round(hls.X / 60, MidpointRounding.AwayFromZero);
if (hls.Y < 0.4)
{
// dark color
switch (hue)
{
case 1: return ConsoleColor.DarkYellow;
case 2: return ConsoleColor.DarkGreen;
case 3: return ConsoleColor.DarkCyan;
case 4: return ConsoleColor.DarkBlue;
case 5: return ConsoleColor.DarkMagenta;
default: return ConsoleColor.DarkRed;
}
}
// bright color
switch (hue)
{
case 1: return ConsoleColor.Yellow;
case 2: return ConsoleColor.Green;
case 3: return ConsoleColor.Cyan;
case 4: return ConsoleColor.Blue;
case 5: return ConsoleColor.Magenta;
default: return ConsoleColor.Red;
}
}
}
}