diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index cb693e87c7..797afd89aa 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -78,6 +78,7 @@
+
@@ -108,7 +109,12 @@
+
+
+
+
+
@@ -153,4 +159,5 @@
+
\ No newline at end of file
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index 23fede6ba9..554fd8bd2f 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -28,11 +28,16 @@ using Content.Server.GameObjects.Components.Weapon.Melee;
using Content.Server.GameObjects.Components.Materials;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.Components.Construction;
+using Content.Server.Players;
+using Content.Server.Mobs;
+using Content.Server.GameObjects.Components.Mobs;
namespace Content.Server
{
public class EntryPoint : GameServer
{
+ const string PlayerPrototypeName = "HumanMob_Content";
+
private IBaseServer _server;
private IPlayerManager _players;
private IEntityManager entityManager;
@@ -40,6 +45,8 @@ namespace Content.Server
private bool _countdownStarted;
+ private GridLocalCoordinates SpawnPoint;
+
///
public override void Init()
{
@@ -52,7 +59,6 @@ namespace Content.Server
_server.RunLevelChanged += HandleRunLevelChanged;
_players.PlayerStatusChanged += HandlePlayerStatusChanged;
- _players.PlayerPrototypeName = "HumanMob_Content";
var factory = IoCManager.Resolve();
@@ -106,6 +112,8 @@ namespace Content.Server
factory.Register();
factory.Register();
factory.RegisterIgnore("ConstructionGhost");
+
+ factory.Register();
}
///
@@ -132,7 +140,7 @@ namespace Content.Server
var newMap = mapMan.CreateMap();
var grid = mapLoader.LoadBlueprint(newMap, "Maps/stationstation.yml");
- _players.FallbackSpawnPoint = new GridLocalCoordinates(Vector2.Zero, grid);
+ SpawnPoint = new GridLocalCoordinates(Vector2.Zero, grid);
var startTime = timing.RealTime;
var timeSpan = timing.RealTime - startTime;
@@ -152,10 +160,16 @@ namespace Content.Server
private void HandlePlayerStatusChanged(object sender, SessionStatusEventArgs args)
{
+ var session = args.Session;
+
switch (args.NewStatus)
{
case SessionStatus.Connected:
{
+ if (session.Data.ContentDataUncast == null)
+ {
+ session.Data.ContentDataUncast = new PlayerData(session.SessionId);
+ }
// timer time must be > tick length
Timer.Spawn(250, args.Session.JoinLobby);
@@ -183,15 +197,22 @@ namespace Content.Server
case SessionStatus.InGame:
{
//TODO: Check for existing mob and re-attach
- var session = args.Session;
- if (session.Data.AttachedEntityUid.HasValue
- && entityManager.TryGetEntity(session.Data.AttachedEntityUid.Value, out var entity))
+ var data = session.ContentData();
+ if (data.Mind == null)
{
- session.AttachToEntity(entity);
+ // No mind yet (new session), make a new one.
+ data.Mind = new Mind(session.SessionId);
+ var mob = SpawnPlayerMob();
+ data.Mind.TransferTo(mob);
}
else
{
- _players.SpawnPlayerMob(args.Session);
+ if (data.Mind.CurrentMob == null)
+ {
+ var mob = SpawnPlayerMob();
+ data.Mind.TransferTo(mob);
+ }
+ session.AttachToEntity(data.Mind.CurrentEntity);
}
chatManager.DispatchMessage(ChatChannel.Server, "Gamemode: Player joined Game!", args.Session.SessionId);
}
@@ -204,5 +225,10 @@ namespace Content.Server
break;
}
}
+
+ IEntity SpawnPlayerMob()
+ {
+ return entityManager.ForceSpawnEntityAt(PlayerPrototypeName, SpawnPoint);
+ }
}
}
diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs
new file mode 100644
index 0000000000..4888fecff8
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Content.Server.Mobs;
+using SS14.Server.GameObjects;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.Network;
+using SS14.Shared.Log;
+
+namespace Content.Server.GameObjects.Components.Mobs
+{
+ ///
+ /// Stores a on a mob.
+ ///
+ public class MindComponent : Component
+ {
+ ///
+ public override string Name => "Mind";
+
+ ///
+ /// The mind controlling this mob. Can be null.
+ ///
+ public Mind Mind { get; private set; }
+
+ ///
+ /// True if we have a mind, false otherwise.
+ ///
+ public bool HasMind => Mind != null;
+
+ ///
+ /// Don't call this unless you know what the hell you're doing.
+ /// Use instead.
+ /// If that doesn't cover it, make something to cover it.
+ ///
+ public void InternalEjectMind()
+ {
+ Mind = null;
+ }
+
+ ///
+ /// Don't call this unless you know what the hell you're doing.
+ /// Use instead.
+ /// If that doesn't cover it, make something to cover it.
+ ///
+ public void InternalAssignMind(Mind value)
+ {
+ Mind = value;
+ }
+
+ public override void OnRemove()
+ {
+ base.OnRemove();
+
+ if (HasMind)
+ {
+ Mind.TransferTo(null);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Mobs/Commands.cs b/Content.Server/Mobs/Commands.cs
new file mode 100644
index 0000000000..a29207cb3b
--- /dev/null
+++ b/Content.Server/Mobs/Commands.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Content.Server.Players;
+using SS14.Server.Interfaces.Console;
+using SS14.Server.Interfaces.Player;
+using SS14.Shared.Interfaces.Reflection;
+using SS14.Shared.IoC;
+using SS14.Shared.Network;
+
+namespace Content.Server.Mobs
+{
+ public class MindInfoCommand : IClientCommand
+ {
+ public string Command => "mindinfo";
+
+ public string Description => "Lists info for the mind of a specific player.";
+
+ public string Help => "mindinfo ";
+
+ public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
+ {
+ if (args.Length != 1)
+ {
+ shell.SendText(player, "Expected exactly 1 argument.");
+ return;
+ }
+
+ var mgr = IoCManager.Resolve();
+ if (mgr.TryGetPlayerData(new NetSessionId(args[0]), out var data))
+ {
+ var mind = data.ContentData().Mind;
+
+ var builder = new StringBuilder();
+ builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.SessionId, mind.CurrentMob?.Owner?.Uid);
+ foreach (var role in mind.AllRoles)
+ {
+ builder.AppendFormat("{0} ", role.Name);
+ }
+
+ shell.SendText(player, builder.ToString());
+ }
+ else
+ {
+ shell.SendText(player, "Can't find that mind");
+ }
+ }
+ }
+
+ public class AddRoleCommand : IClientCommand
+ {
+ public string Command => "addrole";
+
+ public string Description => "Adds a role to a player's mind.";
+
+ public string Help => "addrole \nThat role type is the actual C# type name.";
+
+ public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
+ {
+ if (args.Length != 2)
+ {
+ shell.SendText(player, "Expected exactly 2 arguments.");
+ return;
+ }
+
+ var mgr = IoCManager.Resolve();
+ if (mgr.TryGetPlayerData(new NetSessionId(args[0]), out var data))
+ {
+ var mind = data.ContentData().Mind;
+ var refl = IoCManager.Resolve();
+ var type = refl.LooseGetType(args[1]);
+ mind.AddRole(type);
+ }
+ else
+ {
+ shell.SendText(player, "Can't find that mind");
+ }
+ }
+ }
+
+ public class RemoveRoleCommand : IClientCommand
+ {
+ public string Command => "rmrole";
+
+ public string Description => "Removes a role from a player's mind.";
+
+ public string Help => "rmrole \nThat role type is the actual C# type name.";
+
+ public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
+ {
+ if (args.Length != 2)
+ {
+ shell.SendText(player, "Expected exactly 2 arguments.");
+ return;
+ }
+
+ var mgr = IoCManager.Resolve();
+ if (mgr.TryGetPlayerData(new NetSessionId(args[0]), out var data))
+ {
+ var mind = data.ContentData().Mind;
+ var refl = IoCManager.Resolve();
+ var type = refl.LooseGetType(args[1]);
+ mind.RemoveRole(type);
+ }
+ else
+ {
+ shell.SendText(player, "Can't find that mind");
+ }
+ }
+ }
+}
diff --git a/Content.Server/Mobs/Mind.cs b/Content.Server/Mobs/Mind.cs
new file mode 100644
index 0000000000..2ac536c79a
--- /dev/null
+++ b/Content.Server/Mobs/Mind.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.GameObjects.Components.Mobs;
+using SS14.Server.Interfaces.Player;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
+using SS14.Shared.Network;
+
+namespace Content.Server.Mobs
+{
+ ///
+ /// A mind represents the IC "mind" of a player. Stores roles currently.
+ ///
+ ///
+ /// Think of it like this: if a player is supposed to have their memories,
+ /// their mind follows along.
+ ///
+ /// Things such as respawning do not follow, because you're a new character.
+ /// Getting borged, cloned, turned into a catbeast, etc... will keep it following you.
+ ///
+ public sealed class Mind
+ {
+ private readonly Dictionary _roles = new Dictionary();
+
+ ///
+ /// Creates the new mind attached to a specific player session.
+ ///
+ /// The session ID of the owning player.
+ public Mind(NetSessionId sessionId)
+ {
+ SessionId = sessionId;
+ }
+
+ // TODO: This session should be able to be changed, probably.
+ ///
+ /// The session ID of the player owning this mind.
+ ///
+ public NetSessionId SessionId { get; }
+
+ ///
+ /// The component currently owned by this mind.
+ /// Can be null.
+ ///
+ public MindComponent CurrentMob { get; private set; }
+
+ ///
+ /// The entity currently owned by this mind.
+ /// Can be null.
+ ///
+ public IEntity CurrentEntity => CurrentMob?.Owner;
+
+ ///
+ /// An enumerable over all the roles this mind has.
+ ///
+ public IEnumerable AllRoles => _roles.Values;
+
+ ///
+ /// The session of the player owning this mind.
+ /// Can be null, in which case the player is currently not logged in.
+ ///
+ public IPlayerSession Session
+ {
+ get
+ {
+ var playerMgr = IoCManager.Resolve();
+ playerMgr.TryGetSessionById(SessionId, out var ret);
+ return ret;
+ }
+ }
+
+ ///
+ /// Gives this mind a new role.
+ ///
+ /// The type of the role to give.
+ /// The instance of the role.
+ ///
+ /// Thrown if we already have a role with this type.
+ ///
+ public T AddRole() where T : Role
+ {
+ return (T)AddRole(typeof(T));
+ }
+
+ ///
+ /// Gives this mind a new role.
+ ///
+ /// The type of the role to give.
+ /// The instance of the role.
+ ///
+ /// Thrown if we already have a role with this type.
+ ///
+ public Role AddRole(Type t)
+ {
+ if (_roles.ContainsKey(t))
+ {
+ throw new ArgumentException($"We already have this role: {t}");
+ }
+
+ var role = (Role)Activator.CreateInstance(t, this);
+ _roles[t] = role;
+ role.Greet();
+ return role;
+ }
+
+ ///
+ /// Removes a role from this mind.
+ ///
+ /// The type of the role to remove.
+ ///
+ /// Thrown if we do not have this role.
+ ///
+ public void RemoveRole() where T : Role
+ {
+ RemoveRole(typeof(T));
+ }
+
+ ///
+ /// Removes a role from this mind.
+ ///
+ /// The type of the role to remove.
+ ///
+ /// Thrown if we do not have this role.
+ ///
+ public void RemoveRole(Type t)
+ {
+ if (!_roles.ContainsKey(t))
+ {
+ throw new ArgumentException($"We do not have this role: {t}");
+ }
+
+ // This can definitely get more complex removal hooks later,
+ // when we need it.
+ _roles.Remove(t);
+ }
+
+ ///
+ /// Gets a role of a certain type.
+ ///
+ /// The type of the role to get.
+ /// The role's instance.
+ ///
+ /// Thrown if we do not have a role of this type.
+ ///
+ public T GetRole() where T : Role
+ {
+ return (T)_roles[typeof(T)];
+ }
+
+ ///
+ /// Gets a role of a certain type.
+ ///
+ /// The type of the role to get.
+ /// The role's instance.
+ ///
+ /// Thrown if we do not have a role of this type.
+ ///
+ public Role GetRole(Type t)
+ {
+ return _roles[t];
+ }
+
+ ///
+ /// Transfer this mind's control over to a new entity.
+ ///
+ ///
+ /// The entity to control.
+ /// Can be null, in which case it will simply detach the mind from any entity.
+ ///
+ ///
+ /// Thrown if is already owned by another mind.
+ ///
+ public void TransferTo(IEntity entity)
+ {
+ MindComponent component = null;
+ if (entity != null)
+ {
+ if (!entity.TryGetComponent(out component))
+ {
+ component = entity.AddComponent();
+ }
+ else if (component.HasMind)
+ {
+ // TODO: Kick them out, maybe?
+ throw new ArgumentException("That entity already has a mind.", nameof(entity));
+ }
+ }
+
+ CurrentMob?.InternalEjectMind();
+ CurrentMob = component;
+ CurrentMob?.InternalAssignMind(this);
+
+ // Player is CURRENTLY connected.
+ if (Session != null && CurrentMob != null)
+ {
+ Session.AttachToEntity(entity);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Mobs/Role.cs b/Content.Server/Mobs/Role.cs
new file mode 100644
index 0000000000..c7f0c617a7
--- /dev/null
+++ b/Content.Server/Mobs/Role.cs
@@ -0,0 +1,36 @@
+// Hey look,
+// Antag Datums.
+
+namespace Content.Server.Mobs
+{
+ ///
+ /// The Role is a basic building block for,
+ /// well, IC roles.
+ /// This can be anything and is not necessarily limited to antagonists.
+ ///
+ public abstract class Role
+ {
+ ///
+ /// The mind owning this role instance.
+ ///
+ public Mind Mind { get; }
+
+ ///
+ /// A friendly name for this role type.
+ ///
+ public abstract string Name { get; }
+
+ protected Role(Mind mind)
+ {
+ Mind = mind;
+ }
+
+ ///
+ /// Called when a mind (player) first gets this role, to greet them.
+ ///
+ public virtual void Greet()
+ {
+
+ }
+ }
+}
diff --git a/Content.Server/Mobs/Roles/Traitor.cs b/Content.Server/Mobs/Roles/Traitor.cs
new file mode 100644
index 0000000000..dad5396a8c
--- /dev/null
+++ b/Content.Server/Mobs/Roles/Traitor.cs
@@ -0,0 +1,24 @@
+using SS14.Server.Interfaces.Chat;
+using SS14.Shared.Console;
+using SS14.Shared.IoC;
+
+namespace Content.Server.Mobs.Roles
+{
+ public sealed class Traitor : Role
+ {
+ public Traitor(Mind mind) : base(mind)
+ {
+ }
+
+ public override string Name => "Traitor";
+
+ public override void Greet()
+ {
+ base.Greet();
+
+ var chat = IoCManager.Resolve();
+ chat.DispatchMessage(Mind.Session.ConnectedClient, ChatChannel.Server,
+ "You're a traitor. Go fuck something up. Or something. I don't care to be honest.");
+ }
+ }
+}
diff --git a/Content.Server/Players/PlayerData.cs b/Content.Server/Players/PlayerData.cs
new file mode 100644
index 0000000000..f7dbf25fde
--- /dev/null
+++ b/Content.Server/Players/PlayerData.cs
@@ -0,0 +1,47 @@
+using Content.Server.Mobs;
+using SS14.Server.Interfaces.Player;
+using SS14.Shared.Network;
+
+namespace Content.Server.Players
+{
+ ///
+ /// Content side for all data that tracks a player session.
+ /// Use to retrieve this from an .
+ ///
+ public sealed class PlayerData
+ {
+ ///
+ /// The session ID of the player owning this data.
+ ///
+ public NetSessionId SessionId { get; }
+
+ ///
+ /// The currently occupied mind of the player owning this data.
+ ///
+ public Mind Mind { get; set; }
+
+ public PlayerData(NetSessionId sessionId)
+ {
+ SessionId = sessionId;
+ }
+ }
+
+ public static class PlayerDataExt
+ {
+ ///
+ /// Gets the correctly cast instance of content player data from an engine player data storage.
+ ///
+ public static PlayerData ContentData(this IPlayerData data)
+ {
+ return (PlayerData)data.ContentDataUncast;
+ }
+
+ ///
+ /// Gets the correctly cast instance of content player data from an engine player data storage.
+ ///
+ public static PlayerData ContentData(this IPlayerSession session)
+ {
+ return session.Data.ContentData();
+ }
+ }
+}
diff --git a/engine b/engine
index d8a5a617fe..22fdca62bb 160000
--- a/engine
+++ b/engine
@@ -1 +1 @@
-Subproject commit d8a5a617fef275ed61e0ee41e01a9cbca9cfad87
+Subproject commit 22fdca62bb1a7ea01318d2ecf64514f0f8fe5531