Files
LuaCsForBarotraumaEP/Subsurface/Source/Characters/CharacterNetworking.cs

852 lines
31 KiB
C#

using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class CharacterStateInfo : PosInfo
{
public readonly Direction Direction;
public readonly Entity Interact; //the entity being interacted with
public readonly AnimController.Animation Animation;
public CharacterStateInfo(Vector2 pos, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, 0, time, dir, interact, animation)
{
}
public CharacterStateInfo(Vector2 pos, UInt16 ID, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, ID, 0.0f, dir, interact, animation)
{
}
protected CharacterStateInfo(Vector2 pos, UInt16 ID, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: base(pos, ID, time)
{
Direction = dir;
Interact = interact;
Animation = animation;
}
}
partial class Character
{
[Flags]
private enum InputNetFlags : ushort
{
None = 0x0,
Left = 0x1,
Right = 0x2,
Up = 0x4,
Down = 0x8,
FacingLeft = 0x10,
Run = 0x20,
Crouch = 0x40,
Select = 0x80,
Use = 0x100,
Aim = 0x200,
Attack = 0x400,
MaxVal = 0x7FF
}
private InputNetFlags dequeuedInput = 0;
private InputNetFlags prevDequeuedInput = 0;
public UInt16 LastNetworkUpdateID = 0;
/// <summary>
/// ID of the last inputs the server has processed
/// </summary>
public UInt16 LastProcessedID;
private struct NetInputMem
{
public InputNetFlags states; //keys pressed/other boolean states at this step
public UInt16 intAim; //aim angle, represented as an unsigned short where 0=0º, 65535=just a bit under 360º
public UInt16 interact; //id of the entity being interacted with
public UInt16 networkUpdateID;
}
private List<NetInputMem> memInput = new List<NetInputMem>();
private List<CharacterStateInfo> memState = new List<CharacterStateInfo>();
private List<CharacterStateInfo> memLocalState = new List<CharacterStateInfo>();
private bool networkUpdateSent;
public bool isSynced = false;
public List<CharacterStateInfo> MemState
{
get { return memState; }
}
public List<CharacterStateInfo> MemLocalState
{
get { return memLocalState; }
}
private void UpdateNetInput()
{
if (this != Character.Controlled)
{
if (GameMain.Client != null)
{
//freeze AI characters if more than 1 seconds have passed since last update from the server
if (lastRecvPositionUpdateTime < NetTime.Now - 1.0f)
{
AnimController.Frozen = true;
memState.Clear();
//hide after 2 seconds
if (lastRecvPositionUpdateTime < NetTime.Now - 2.0f)
{
Enabled = false;
return;
}
}
}
else if (GameMain.Server != null && !(this is AICharacter))
{
if (!AllowInput)
{
AnimController.Frozen = false;
}
else if (memInput.Count == 0)
{
AnimController.Frozen = true;
}
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 = (ViewTarget == null ? AnimController.Collider.Position : ViewTarget.Position)
+ new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
var closestEntity = Entity.FindEntityByID(memInput[memInput.Count - 1].interact);
if (closestEntity is Item)
{
closestItem = (Item)closestEntity;
closestCharacter = null;
}
else if (closestEntity is Character)
{
closestCharacter = (Character)closestEntity;
closestItem = 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);
}
}
}
}
}
else if (GameMain.Client != null)
{
var posInfo = new CharacterStateInfo(
SimPosition,
LastNetworkUpdateID,
AnimController.TargetDir,
selectedCharacter == null ? (Entity)selectedConstruction : (Entity)selectedCharacter,
AnimController.Anim);
memLocalState.Add(posInfo);
InputNetFlags newInput = InputNetFlags.None;
if (IsKeyDown(InputType.Left)) newInput |= InputNetFlags.Left;
if (IsKeyDown(InputType.Right)) newInput |= InputNetFlags.Right;
if (IsKeyDown(InputType.Up)) newInput |= InputNetFlags.Up;
if (IsKeyDown(InputType.Down)) newInput |= InputNetFlags.Down;
if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run;
if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch;
if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered
if (IsKeyDown(InputType.Use)) newInput |= InputNetFlags.Use;
if (IsKeyDown(InputType.Aim)) newInput |= InputNetFlags.Aim;
if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack;
if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft;
Vector2 relativeCursorPos = cursorPosition - (ViewTarget == null ? AnimController.Collider.Position : ViewTarget.Position);
relativeCursorPos.Normalize();
UInt16 intAngle = (UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI));
NetInputMem newMem = new NetInputMem();
newMem.states = newInput;
newMem.intAim = intAngle;
if (closestItem != null)
{
newMem.interact = closestItem.ID;
}
else if (closestCharacter != null)
{
newMem.interact = closestCharacter.ID;
}
memInput.Insert(0, newMem);
LastNetworkUpdateID++;
if (memInput.Count > 60)
{
memInput.RemoveRange(60, memInput.Count - 60);
}
}
else //this == Character.Controlled && GameMain.Client == null
{
AnimController.Frozen = false;
}
if (networkUpdateSent)
{
foreach (Key key in keys)
{
key.DequeueHit();
key.DequeueHeld();
}
networkUpdateSent = false;
}
}
public virtual void ClientWrite(NetBuffer msg, object[] extraData = null)
{
if (GameMain.Server != null) return;
if (extraData != null)
{
switch ((NetEntityEvent.Type)extraData[0])
{
case NetEntityEvent.Type.InventoryState:
msg.WriteRangedInteger(0, 2, 0);
inventory.ClientWrite(msg, extraData);
break;
case NetEntityEvent.Type.Repair:
msg.WriteRangedInteger(0, 2, 1);
msg.Write(AnimController.Anim == AnimController.Animation.CPR);
break;
case NetEntityEvent.Type.Status:
msg.WriteRangedInteger(0, 2, 2);
break;
}
msg.WritePadBits();
}
else
{
msg.Write((byte)ClientNetObject.CHARACTER_INPUT);
if (memInput.Count > 60)
{
memInput.RemoveRange(60, memInput.Count - 60);
}
msg.Write(LastNetworkUpdateID);
byte inputCount = Math.Min((byte)memInput.Count, (byte)60);
msg.Write(inputCount);
for (int i = 0; i < inputCount; i++)
{
msg.WriteRangedInteger(0, (int)InputNetFlags.MaxVal, (int)memInput[i].states);
if (memInput[i].states.HasFlag(InputNetFlags.Aim))
{
msg.Write(memInput[i].intAim);
}
if (memInput[i].states.HasFlag(InputNetFlags.Select) || memInput[i].states.HasFlag(InputNetFlags.Use))
{
msg.Write(memInput[i].interact);
}
}
}
}
public virtual void ServerRead(ClientNetObject type, NetBuffer 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.HasFlag(InputNetFlags.Aim))
{
newAim = msg.ReadUInt16();
}
if (newInput.HasFlag(InputNetFlags.Select) || newInput.HasFlag(InputNetFlags.Use))
{
newInteract = msg.ReadUInt16();
}
if (AllowInput)
{
if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60))
{
NetInputMem newMem = new NetInputMem();
newMem.states = newInput;
newMem.intAim = newAim;
newMem.interact = newInteract;
newMem.networkUpdateID = (ushort)(networkUpdateID - i);
memInput.Insert(i, newMem);
}
}
}
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,2);
switch (eventType)
{
case 0:
inventory.ServerRead(type, msg, c);
break;
case 1:
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
bool doingCPR = msg.ReadBoolean();
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)
{
Kill(lastAttackCauseOfDeath);
}
break;
}
msg.ReadPadBits();
break;
}
}
public virtual void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
{
if (GameMain.Server == null) return;
if (extraData != null)
{
switch ((NetEntityEvent.Type)extraData[0])
{
case NetEntityEvent.Type.InventoryState:
msg.Write(true);
inventory.ClientWrite(msg, extraData);
break;
case NetEntityEvent.Type.Status:
msg.Write(false);
WriteStatus(msg);
break;
}
}
else
{
msg.Write(ID);
NetBuffer tempBuffer = new NetBuffer();
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;
if (IsRemotePlayer)
{
aiming = dequeuedInput.HasFlag(InputNetFlags.Aim);
use = dequeuedInput.HasFlag(InputNetFlags.Use);
attack = dequeuedInput.HasFlag(InputNetFlags.Attack);
}
else
{
aiming = keys[(int)InputType.Aim].GetHeldQueue;
use = keys[(int)InputType.Use].GetHeldQueue;
attack = keys[(int)InputType.Attack].GetHeldQueue;
networkUpdateSent = true;
}
tempBuffer.Write(aiming);
tempBuffer.Write(use);
bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null);
tempBuffer.Write(hasAttackLimb);
if (hasAttackLimb) tempBuffer.Write(attack);
if (aiming)
{
Vector2 relativeCursorPos = cursorPosition - (ViewTarget == null ? AnimController.Collider.Position : ViewTarget.Position);
tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI)));
}
tempBuffer.Write(AnimController.TargetDir == Direction.Right);
}
if (selectedCharacter != null || selectedConstruction != null)
{
tempBuffer.Write(true);
tempBuffer.Write(selectedCharacter != null ? selectedCharacter.ID : selectedConstruction.ID);
if (selectedCharacter != null)
{
tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR);
}
}
else
{
tempBuffer.Write(false);
}
tempBuffer.Write(SimPosition.X);
tempBuffer.Write(SimPosition.Y);
tempBuffer.WritePadBits();
msg.Write((byte)tempBuffer.LengthBytes);
msg.Write(tempBuffer);
}
}
public virtual void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime)
{
if (GameMain.Server != null) return;
switch (type)
{
case ServerNetObject.ENTITY_POSITION:
bool facingRight = AnimController.Dir > 0.0f;
lastRecvPositionUpdateTime = (float)NetTime.Now;
AnimController.Frozen = false;
Enabled = true;
UInt16 networkUpdateID = 0;
if (msg.ReadBoolean())
{
networkUpdateID = msg.ReadUInt16();
}
else
{
bool aimInput = msg.ReadBoolean();
keys[(int)InputType.Aim].Held = aimInput;
keys[(int)InputType.Aim].SetState(false, aimInput);
bool useInput = msg.ReadBoolean();
keys[(int)InputType.Use].Held = useInput;
keys[(int)InputType.Use].SetState(false, useInput);
bool hasAttackLimb = msg.ReadBoolean();
if (hasAttackLimb)
{
bool attackInput = msg.ReadBoolean();
keys[(int)InputType.Attack].Held = attackInput;
keys[(int)InputType.Attack].SetState(false, attackInput);
}
if (aimInput)
{
double aimAngle = ((double)msg.ReadUInt16() / 65535.0) * 2.0 * Math.PI;
cursorPosition = (ViewTarget == null ? AnimController.Collider.Position : ViewTarget.Position)
+ new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
TransformCursorPos();
}
facingRight = msg.ReadBoolean();
}
bool entitySelected = msg.ReadBoolean();
Entity selectedEntity = null;
AnimController.Animation animation = AnimController.Animation.None;
if (entitySelected)
{
ushort entityID = msg.ReadUInt16();
selectedEntity = FindEntityByID(entityID);
if (selectedEntity is Character)
{
bool doingCpr = msg.ReadBoolean();
if (doingCpr && selectedCharacter != null)
{
animation = AnimController.Animation.CPR;
}
}
}
Vector2 pos = new Vector2(
msg.ReadFloat(),
msg.ReadFloat());
int index = 0;
if (GameMain.NetworkMember.Character == this && AllowInput)
{
var posInfo = new CharacterStateInfo(pos, networkUpdateID, facingRight ? Direction.Right : Direction.Left, selectedEntity, animation);
while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID))
index++;
memState.Insert(index, posInfo);
}
else
{
var posInfo = new CharacterStateInfo(pos, sendingTime, facingRight ? Direction.Right : Direction.Left, selectedEntity, animation);
while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp)
index++;
memState.Insert(index, posInfo);
}
break;
case ServerNetObject.ENTITY_EVENT:
bool isInventoryUpdate = msg.ReadBoolean();
if (isInventoryUpdate)
{
inventory.ClientRead(type, msg, sendingTime);
}
else
{
ReadStatus(msg);
}
break;
}
}
private void WriteStatus(NetBuffer msg)
{
if (GameMain.Client != null)
{
DebugConsole.ThrowError("Client attempted to write character status to a networked message");
return;
}
msg.Write(isDead);
if (isDead)
{
msg.Write((byte)causeOfDeath);
}
else
{
msg.WriteRangedSingle(health, minHealth, maxHealth, 8);
msg.Write(oxygen < 100.0f);
if (oxygen < 100.0f)
{
msg.WriteRangedSingle(oxygen, -100.0f, 100.0f, 8);
}
msg.Write(bleeding > 0.0f);
if (bleeding > 0.0f)
{
msg.WriteRangedSingle(bleeding, 0.0f, 5.0f, 8);
}
msg.Write(Stun > 0.0f);
if (Stun > 0.0f)
{
Stun = MathHelper.Clamp(Stun, 0.0f, 60.0f);
msg.WriteRangedSingle(Stun, 0.0f, 60.0f, 8);
}
msg.Write(HuskInfectionState > 0.0f);
}
}
private void ReadStatus(NetBuffer msg)
{
if (GameMain.Server != null)
{
DebugConsole.ThrowError("Server attempted to read character status from a networked message");
return;
}
bool isDead = msg.ReadBoolean();
if (isDead)
{
causeOfDeath = (CauseOfDeath)msg.ReadByte();
if (causeOfDeath == CauseOfDeath.Pressure)
{
Implode(true);
}
else
{
Kill(causeOfDeath, true);
}
}
else
{
health = msg.ReadRangedSingle(minHealth, maxHealth, 8);
bool lowOxygen = msg.ReadBoolean();
if (lowOxygen)
{
Oxygen = msg.ReadRangedSingle(-100.0f, 100.0f, 8);
}
else
{
Oxygen = 100.0f;
}
bool isBleeding = msg.ReadBoolean();
if (isBleeding)
{
bleeding = msg.ReadRangedSingle(0.0f, 5.0f, 8);
}
else
{
bleeding = 0.0f;
}
bool stunned = msg.ReadBoolean();
if (stunned)
{
float newStunTimer = msg.ReadRangedSingle(0.0f, 60.0f, 8);
SetStun(newStunTimer, true, true);
}
else
{
SetStun(0.0f, true, true);
}
bool huskInfected = msg.ReadBoolean();
if (huskInfected)
{
HuskInfectionState = Math.Max(HuskInfectionState, 0.01f);
}
else
{
HuskInfectionState = 0.0f;
}
}
}
public void WriteSpawnData(NetBuffer msg)
{
if (GameMain.Server == null) return;
msg.Write(Info == null);
msg.Write(ID);
msg.Write(ConfigPath);
msg.Write(WorldPosition.X);
msg.Write(WorldPosition.Y);
msg.Write(Enabled);
//character with no characterinfo (e.g. some monster)
if (Info == null) 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(Info.Name);
msg.Write(TeamID);
msg.Write(this is AICharacter);
msg.Write(Info.Gender == Gender.Female);
msg.Write((byte)Info.HeadSpriteId);
if (info.Job != null)
{
msg.Write(Info.Job.Name);
msg.Write((byte)info.Job.Skills.Count);
foreach (Skill skill in info.Job.Skills)
{
msg.WriteRangedInteger(0, 100, MathHelper.Clamp(skill.Level, 0, 100));
}
}
else
{
msg.Write("");
}
}
public static Character ReadSpawnData(NetBuffer inc, bool spawn = true)
{
if (GameMain.Server != null) return null;
bool noInfo = inc.ReadBoolean();
ushort id = inc.ReadUInt16();
string configPath = inc.ReadString();
Vector2 position = new Vector2(inc.ReadFloat(), inc.ReadFloat());
bool enabled = inc.ReadBoolean();
DebugConsole.Log("Received spawn data for " + configPath);
Character character = null;
if (noInfo)
{
if (!spawn) return null;
character = Character.Create(configPath, position, null, true);
character.ID = id;
}
else
{
bool hasOwner = inc.ReadBoolean();
int ownerId = hasOwner ? inc.ReadByte() : -1;
string newName = inc.ReadString();
byte teamID = inc.ReadByte();
bool hasAi = inc.ReadBoolean();
bool isFemale = inc.ReadBoolean();
int headSpriteID = inc.ReadByte();
string jobName = inc.ReadString();
JobPrefab jobPrefab = null;
List<int> skillLevels = new List<int>();
if (!string.IsNullOrEmpty(jobName))
{
jobPrefab = JobPrefab.List.Find(jp => jp.Name == jobName);
int skillCount = inc.ReadByte();
for (int i = 0; i < skillCount; i++)
{
skillLevels.Add(inc.ReadRangedInteger(0, 100));
}
}
if (!spawn) return null;
CharacterInfo ch = new CharacterInfo(configPath, newName, isFemale ? Gender.Female : Gender.Male, jobPrefab);
ch.HeadSpriteId = headSpriteID;
System.Diagnostics.Debug.Assert(skillLevels.Count == ch.Job.Skills.Count);
if (ch.Job != null)
{
for (int i = 0; i < skillLevels.Count && i < ch.Job.Skills.Count; i++)
{
ch.Job.Skills[i].Level = skillLevels[i];
}
}
character = Create(configPath, position, ch, GameMain.Client.ID != ownerId, hasAi);
character.ID = id;
character.TeamID = teamID;
if (GameMain.Client.ID == ownerId)
{
GameMain.Client.Character = character;
Controlled = character;
GameMain.LightManager.LosEnabled = true;
character.memInput.Clear();
character.memState.Clear();
character.memLocalState.Clear();
}
else
{
var ownerClient = GameMain.Client.ConnectedClients.Find(c => c.ID == ownerId);
if (ownerClient != null)
{
ownerClient.Character = character;
}
}
if (configPath == Character.HumanConfigFile)
{
GameMain.GameSession.CrewManager.characters.Add(character);
}
}
character.Enabled = Controlled == character || enabled;
return character;
}
}
}