(61d00a474) v0.9.7.1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+320
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+567
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+694
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -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);
|
||||
}
|
||||
}
|
||||
+578
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user