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