]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Mind Role Entities (#31318)
authorErrant <35878406+Errant-4@users.noreply.github.com>
Thu, 10 Oct 2024 08:48:56 +0000 (10:48 +0200)
committerGitHub <noreply@github.com>
Thu, 10 Oct 2024 08:48:56 +0000 (10:48 +0200)
* Mind Role Entities wip

* headrev count fix

* silicon stuff, cleanup

* exclusive antag config, cleanup

* jobroleadd overwerite

* logging stuff

* MindHasRole cleanup, admin log stuff

* last second cleanup

* ocd

* minor cleanup

* remove createdTime datafield

* now actually using the event replacement I made for role time tracking

* weh

65 files changed:
Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
Content.IntegrationTests/Tests/Internals/AutoInternalsTests.cs
Content.IntegrationTests/Tests/Minds/MindTests.cs
Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs
Content.Server/Antag/AntagSelectionSystem.cs
Content.Server/Antag/Components/AntagSelectionComponent.cs
Content.Server/Cloning/CloningSystem.cs
Content.Server/EntityEffects/EffectConditions/JobCondition.cs
Content.Server/GameTicking/GameTicker.RoundFlow.cs
Content.Server/GameTicking/GameTicker.Spawning.cs
Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
Content.Server/GameTicking/Rules/ThiefRuleSystem.cs
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
Content.Server/Ghost/Roles/GhostRoleMarkerRoleComponent.cs
Content.Server/Ghost/Roles/GhostRoleSystem.cs
Content.Server/Mind/Commands/MindInfoCommand.cs
Content.Server/Objectives/Systems/KillPersonConditionSystem.cs
Content.Server/Objectives/Systems/NinjaConditionsSystem.cs
Content.Server/Objectives/Systems/NotCommandRequirementSystem.cs
Content.Server/Objectives/Systems/NotJobRequirementSystem.cs
Content.Server/Objectives/Systems/RoleRequirementSystem.cs
Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
Content.Server/Revolutionary/Components/CommandStaffComponent.cs
Content.Server/Roles/DragonRoleComponent.cs
Content.Server/Roles/InitialInfectedRoleComponent.cs
Content.Server/Roles/Jobs/JobSystem.cs
Content.Server/Roles/NinjaRoleComponent.cs
Content.Server/Roles/NukeopsRoleComponent.cs
Content.Server/Roles/RemoveRoleCommand.cs
Content.Server/Roles/RevolutionaryRoleComponent.cs
Content.Server/Roles/RoleSystem.cs
Content.Server/Roles/SubvertedSiliconRoleComponent.cs
Content.Server/Roles/ThiefRoleComponent.cs
Content.Server/Roles/TraitorRoleComponent.cs
Content.Server/Roles/ZombieRoleComponent.cs
Content.Server/Silicons/Laws/SiliconLawSystem.cs
Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs
Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs
Content.Server/Station/Systems/StationSpawningSystem.cs
Content.Server/Store/Conditions/BuyerAntagCondition.cs
Content.Server/Store/Conditions/BuyerDepartmentCondition.cs
Content.Server/Store/Conditions/BuyerJobCondition.cs
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Shared/Mind/Components/MindContainerComponent.cs
Content.Shared/Mind/MindComponent.cs
Content.Shared/Roles/AntagonistRoleComponent.cs [deleted file]
Content.Shared/Roles/Jobs/JobComponent.cs [deleted file]
Content.Shared/Roles/Jobs/JobRoleComponent.cs [new file with mode: 0644]
Content.Shared/Roles/Jobs/SharedJobSystem.cs
Content.Shared/Roles/MindGetAllRoleInfoEvent.cs [moved from Content.Shared/Roles/MindGetAllRolesEvent.cs with 79% similarity]
Content.Shared/Roles/MindRoleComponent.cs [new file with mode: 0644]
Content.Shared/Roles/SharedRoleSystem.cs
Content.Shared/Whitelist/EntityWhitelist.cs
Content.Shared/Whitelist/EntityWhitelistSystem.cs
Resources/Locale/en-US/game-ticking/game-presets/preset-nukeops.ftl
Resources/Prototypes/GameRules/events.yml
Resources/Prototypes/GameRules/midround.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/Objectives/dragon.yml
Resources/Prototypes/Objectives/ninja.yml
Resources/Prototypes/Objectives/thief.yml
Resources/Prototypes/Objectives/traitor.yml
Resources/Prototypes/Roles/MindRoles/mind_roles.yml [new file with mode: 0644]

index a4563aa37e6e64efdb3feeb8790cb400529f4641..039c0c7b184df1d2cb3196c0cde0717f494eff6f 100644 (file)
@@ -1,4 +1,5 @@
 #nullable enable
+using System.Collections.Generic;
 using System.Linq;
 using Content.Server.Body.Components;
 using Content.Server.GameTicking;
@@ -120,8 +121,8 @@ public sealed class NukeOpsTest
         Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
         Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
         Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
-        var roles = roleSys.MindGetAllRoles(mind);
-        var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
+        var roles = roleSys.MindGetAllRoleInfo(mind);
+        var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
         Assert.That(cmdRoles.Count(), Is.EqualTo(1));
 
         // The second dummy player should be a medic
@@ -131,8 +132,8 @@ public sealed class NukeOpsTest
         Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
         Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
         Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
-        roles = roleSys.MindGetAllRoles(dummyMind);
-        cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic" && x.Component is NukeopsRoleComponent);
+        roles = roleSys.MindGetAllRoleInfo(dummyMind);
+        cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
         Assert.That(cmdRoles.Count(), Is.EqualTo(1));
 
         // The other two players should have just spawned in as normal.
@@ -141,13 +142,14 @@ public sealed class NukeOpsTest
         void CheckDummy(int i)
         {
             var ent = dummyEnts[i];
-            var mind = mindSys.GetMind(ent)!.Value;
+            var mindCrew = mindSys.GetMind(ent)!.Value;
             Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
-            Assert.That(roleSys.MindIsAntagonist(mind), Is.False);
-            Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind), Is.False);
+            Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
+            Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
             Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
             Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
-            Assert.That(roleSys.MindGetAllRoles(mind).Any(x => x.Component is NukeopsRoleComponent), Is.False);
+            var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
+            Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
         }
 
         // The game rule exists, and all the stations/shuttles/maps are properly initialized
@@ -238,7 +240,8 @@ public sealed class NukeOpsTest
             for (var i = 0; i < nukies.Length - 1; i++)
             {
                 entMan.DeleteEntity(nukies[i]);
-                Assert.That(roundEndSys.IsRoundEndRequested, Is.False,
+                Assert.That(roundEndSys.IsRoundEndRequested,
+                    Is.False,
                     $"The round ended, but {nukies.Length - i - 1} nukies are still alive!");
             }
             // Delete the last nukie and make sure the round ends.
index dbd612c71012a83f1bb5b27f150d7bb5f9ec4c40..d153536873634fbd694e9adb16e8cd651224a831 100644 (file)
@@ -2,7 +2,6 @@ using Content.Server.Atmos.EntitySystems;
 using Content.Server.Body.Systems;
 using Content.Server.Station.Systems;
 using Content.Shared.Preferences;
-using Content.Shared.Roles.Jobs;
 
 namespace Content.IntegrationTests.Tests.Internals;
 
@@ -25,10 +24,7 @@ public sealed class AutoInternalsTests
         await server.WaitAssertion(() =>
         {
             var profile = new HumanoidCharacterProfile();
-            var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, new JobComponent()
-            {
-                Prototype = "TestInternalsDummy"
-            }, profile, station: null);
+            var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, "TestInternalsDummy", profile, station: null);
 
             Assert.That(atmos.HasAtmosphere(testMap.Grid), Is.False, "Test map has atmosphere - test needs adjustment!");
             Assert.That(internals.AreInternalsWorking(dummy), "Internals did not automatically connect!");
index 3b7ecf0135f6589c6b900c0099e0c61304ed5a1b..d4d551f4e0aad0573390ec96f256c3edfa3b0831 100644 (file)
@@ -1,10 +1,8 @@
 #nullable enable
 using System.Linq;
-using Content.Server.Ghost;
 using Content.Server.Ghost.Roles;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Mind.Commands;
-using Content.Server.Players;
 using Content.Server.Roles;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
@@ -18,7 +16,6 @@ using Robust.Server.Console;
 using Robust.Server.GameObjects;
 using Robust.Server.Player;
 using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
 
@@ -287,27 +284,27 @@ public sealed partial class MindTests
             Assert.Multiple(() =>
             {
                 Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
-                Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
+                Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
             });
 
-            var traitorRole = new TraitorRoleComponent();
+            var traitorRole = "MindRoleTraitor";
 
             roleSystem.MindAddRole(mindId, traitorRole);
 
             Assert.Multiple(() =>
             {
                 Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
-                Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
+                Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
             });
 
-            var jobRole = new JobComponent();
+            var jobRole = "";
 
-            roleSystem.MindAddRole(mindId, jobRole);
+            roleSystem.MindAddJobRole(mindId, jobPrototype:jobRole);
 
             Assert.Multiple(() =>
             {
                 Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
-                Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
+                Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
             });
 
             roleSystem.MindRemoveRole<TraitorRoleComponent>(mindId);
@@ -315,15 +312,15 @@ public sealed partial class MindTests
             Assert.Multiple(() =>
             {
                 Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
-                Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
+                Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
             });
 
-            roleSystem.MindRemoveRole<JobComponent>(mindId);
+            roleSystem.MindRemoveRole<JobRoleComponent>(mindId);
 
             Assert.Multiple(() =>
             {
                 Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
-                Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
+                Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
             });
         });
 
index 6746d6d5a9405be3c7d96189933715d631db4cbd..267b3637e0a8f6304419b8e9e44c751959000fa9 100644 (file)
@@ -3,7 +3,6 @@ using Content.Server.Station.Systems;
 using Content.Shared.Inventory;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Roles.Jobs;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
 
@@ -68,10 +67,7 @@ public sealed class LoadoutTests
 
             profile.SetLoadout(new RoleLoadout("LoadoutTester"));
 
-            var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
-            {
-                Prototype = "LoadoutTester"
-            }, profile, station: null);
+            var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: "LoadoutTester", profile, station: null);
 
             var slotQuery = inventorySystem.GetSlotEnumerator(tester);
             var checkedCount = 0;
index 3f33d01116dda97e4db70493310a59ad31c2d155..224629ff2e52261cf20f2deb62b8c06a6e130f49 100644 (file)
@@ -11,7 +11,6 @@ using Content.Server.Preferences.Managers;
 using Content.Server.Roles;
 using Content.Server.Roles.Jobs;
 using Content.Server.Shuttles.Components;
-using Content.Server.Station.Systems;
 using Content.Shared.Antag;
 using Content.Shared.Clothing;
 using Content.Shared.GameTicking;
@@ -20,7 +19,6 @@ using Content.Shared.Ghost;
 using Content.Shared.Humanoid;
 using Content.Shared.Mind;
 using Content.Shared.Players;
-using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Roles;
 using Content.Shared.Whitelist;
 using Robust.Server.Audio;
@@ -37,14 +35,14 @@ namespace Content.Server.Antag;
 
 public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
 {
-    [Dependency] private readonly IChatManager _chat = default!;
-    [Dependency] private readonly IPlayerManager _playerManager = default!;
-    [Dependency] private readonly IServerPreferencesManager _pref = default!;
     [Dependency] private readonly AudioSystem _audio = default!;
+    [Dependency] private readonly IChatManager _chat = default!;
     [Dependency] private readonly GhostRoleSystem _ghostRole = default!;
     [Dependency] private readonly JobSystem _jobs = default!;
     [Dependency] private readonly LoadoutSystem _loadout = default!;
     [Dependency] private readonly MindSystem _mind = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly IServerPreferencesManager _pref = default!;
     [Dependency] private readonly RoleSystem _role = default!;
     [Dependency] private readonly TransformSystem _transform = default!;
     [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
@@ -193,6 +191,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
     /// <summary>
     /// Chooses antagonists from the given selection of players
     /// </summary>
+    /// <param name="ent">The antagonist rule entity</param>
+    /// <param name="pool">The players to choose from</param>
+    /// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
     public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
     {
         if (ent.Comp.SelectionsComplete)
@@ -209,8 +210,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
     /// <summary>
     /// Chooses antagonists from the given selection of players for the given antag definition.
     /// </summary>
+    /// <param name="ent">The antagonist rule entity</param>
+    /// <param name="pool">The players to choose from</param>
+    /// <param name="def">The antagonist selection parameters and criteria</param>
     /// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
-    public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def, bool midround = false)
+    public void ChooseAntags(Entity<AntagSelectionComponent> ent,
+        IList<ICommonSession> pool,
+        AntagSelectionDefinition def,
+        bool midround = false)
     {
         var playerPool = GetPlayerPool(ent, pool, def);
         var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
@@ -331,7 +338,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
         EntityManager.AddComponents(player, def.Components);
 
         // Equip the entity's RoleLoadout and LoadoutGroup
-        List<ProtoId<StartingGearPrototype>>? gear = new();
+        List<ProtoId<StartingGearPrototype>> gear = new();
         if (def.StartingGear is not null)
             gear.Add(def.StartingGear.Value);
 
@@ -340,8 +347,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
         if (session != null)
         {
             var curMind = session.GetMind();
-            
-            if (curMind == null || 
+
+            if (curMind == null ||
                 !TryComp<MindComponent>(curMind.Value, out var mindComp) ||
                 mindComp.OwnedEntity != antagEnt)
             {
@@ -350,7 +357,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
             }
 
             _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
-            _role.MindAddRoles(curMind.Value, def.MindComponents, null, true);
+            _role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
             ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
             SendBriefing(session, def.Briefing);
         }
index 0e5a0afc9bcec4b5809ef4f8796c136063a76573..502fb8eda2c5ff11aedcb8d74536ed519228c22a 100644 (file)
@@ -3,7 +3,6 @@ using Content.Shared.Antag;
 using Content.Shared.Destructible.Thresholds;
 using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Roles;
-using Content.Shared.Storage;
 using Content.Shared.Whitelist;
 using Robust.Shared.Audio;
 using Robust.Shared.Player;
@@ -145,10 +144,17 @@ public partial struct AntagSelectionDefinition()
 
     /// <summary>
     /// Components added to the player's mind.
+    /// Do NOT use this to add role-type components. Add those as MindRoles instead
     /// </summary>
     [DataField]
     public ComponentRegistry MindComponents = new();
 
+    /// <summary>
+    /// List of Mind Role Prototypes to be added to the player's mind.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<EntityPrototype>>? MindRoles;
+
     /// <summary>
     /// A set of starting gear that's equipped to the player.
     /// </summary>
index 3893f31d25d755067bbde4aa3892107ab663fadc..65f927400162976f15f78fb3832d324f6f7f62c6 100644 (file)
@@ -231,7 +231,7 @@ namespace Content.Server.Cloning
 
             // TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
             // Add on special job components to the mob.
-            if (_jobs.MindTryGetJob(mindEnt, out _, out var prototype))
+            if (_jobs.MindTryGetJob(mindEnt, out var prototype))
             {
                 foreach (var special in prototype.Special)
                 {
index 20d67d6de0cd1ee8f8ab7b969102ba353672def0..9c7bda839e421e483373d4a7ea8ddf4d97385833 100644 (file)
@@ -1,49 +1,45 @@
 using System.Linq;
 using Content.Shared.EntityEffects;
-using Content.Shared.Mobs;
-using Content.Shared.Mobs.Components;
 using Content.Shared.Localizations;
-using Robust.Shared.Prototypes;
 using Content.Shared.Mind;
 using Content.Shared.Mind.Components;
 using Content.Shared.Roles;
 using Content.Shared.Roles.Jobs;
-using Content.Shared.Station;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.EntityEffects.EffectConditions;
 
 public sealed partial class JobCondition : EntityEffectCondition
 {
     [DataField(required: true)] public List<ProtoId<JobPrototype>> Job;
-                
+
     public override bool Condition(EntityEffectBaseArgs args)
-    {   
+    {
         args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
-        if (mindContainer != null && mindContainer.Mind != null)
+
+        if ( mindContainer is null
+             || !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
+            return false;
+
+        foreach (var roleId in mind.MindRoles)
         {
-            var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
-            if (args.EntityManager.TryGetComponent<JobComponent>(mindContainer?.Mind, out var comp) && prototypeManager.TryIndex(comp.Prototype, out var prototype))
-            {
-                foreach (var jobId in Job)
-                {
-                    if (prototype.ID == jobId)
-                    {
-                        return true;
-                    }
-                }
-            }
+            if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
+                continue;
+
+            if(!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole)
+               || mindRole.JobPrototype is null)
+                continue;
+
+            if (Job.Contains(mindRole.JobPrototype.Value))
+                return true;
         }
-            
+
         return false;
     }
-        
+
     public override string GuidebookExplanation(IPrototypeManager prototype)
     {
         var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
         return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
     }
 }
-
-
index 0c7668d354d57114497f23f773d2c8bba107af9a..683061d8edcfe52fa083697e867d6d78a33fd517 100644 (file)
@@ -4,6 +4,7 @@ using Content.Server.Discord;
 using Content.Server.GameTicking.Events;
 using Content.Server.Ghost;
 using Content.Server.Maps;
+using Content.Server.Roles;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
 using Content.Shared.GameTicking;
@@ -26,6 +27,7 @@ namespace Content.Server.GameTicking
     public sealed partial class GameTicker
     {
         [Dependency] private readonly DiscordWebhook _discord = default!;
+        [Dependency] private readonly RoleSystem _role = default!;
         [Dependency] private readonly ITaskManager _taskManager = default!;
 
         private static readonly Counter RoundNumberMetric = Metrics.CreateCounter(
@@ -373,7 +375,7 @@ namespace Content.Server.GameTicking
                 var userId = mind.UserId ?? mind.OriginalOwnerUserId;
 
                 var connected = false;
-                var observer = HasComp<ObserverRoleComponent>(mindId);
+                var observer = _role.MindHasRole<ObserverRoleComponent>(mindId);
                 // Continuing
                 if (userId != null && _playerManager.ValidSessionId(userId.Value))
                 {
@@ -400,7 +402,7 @@ namespace Content.Server.GameTicking
                     _pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
                 }
 
-                var roles = _roles.MindGetAllRoles(mindId);
+                var roles = _roles.MindGetAllRoleInfo(mindId);
 
                 var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo()
                 {
index 53dc712daeb9361d7cfd362840ed08704445b194..9c8b04770fd909b84b160fff582ef55434de319d 100644 (file)
@@ -3,8 +3,6 @@ using System.Linq;
 using System.Numerics;
 using Content.Server.Administration.Managers;
 using Content.Server.GameTicking.Events;
-using Content.Server.Ghost;
-using Content.Server.Shuttles.Components;
 using Content.Server.Spawners.Components;
 using Content.Server.Speech.Components;
 using Content.Server.Station.Components;
@@ -224,13 +222,12 @@ namespace Content.Server.GameTicking
             _mind.SetUserId(newMind, data.UserId);
 
             var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
-            var job = new JobComponent {Prototype = jobId};
-            _roles.MindAddRole(newMind, job, silent: silent);
+            _roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
             var jobName = _jobs.MindTryGetJobName(newMind);
 
             _playTimeTrackings.PlayerRolesChanged(player);
 
-            var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character);
+            var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
             DebugTools.AssertNotNull(mobMaybe);
             var mob = mobMaybe!.Value;
 
@@ -269,13 +266,17 @@ namespace Content.Server.GameTicking
             _stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
 
             if (lateJoin)
+            {
                 _adminLogger.Add(LogType.LateJoin,
                     LogImpact.Medium,
                     $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
+            }
             else
+            {
                 _adminLogger.Add(LogType.RoundStartJoin,
                     LogImpact.Medium,
                     $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
+            }
 
             // Make sure they're aware of extended access.
             if (Comp<StationJobsComponent>(station).ExtendedAccess
@@ -361,7 +362,7 @@ namespace Content.Server.GameTicking
                 var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
                 mind = (mindId, mindComp);
                 _mind.SetUserId(mind.Value, player.UserId);
-                _roles.MindAddRole(mind.Value, new ObserverRoleComponent());
+                _roles.MindAddRole(mind.Value, "MindRoleObserver");
             }
 
             var ghost = _ghost.SpawnGhost(mind.Value);
index 57239ee8c153c0d3af649520f9ec67c9975aa614..ca6548301a7ee43a24aa90d06ff2ad949844a958 100644 (file)
@@ -9,7 +9,6 @@ using Content.Server.RoundEnd;
 using Content.Server.Shuttles.Events;
 using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Components;
-using Content.Server.Store.Components;
 using Content.Server.Store.Systems;
 using Content.Shared.GameTicking.Components;
 using Content.Shared.Mobs;
@@ -31,9 +30,9 @@ namespace Content.Server.GameTicking.Rules;
 
 public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 {
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
     [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
-    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly PopupSystem _popupSystem = default!;
     [Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
     [Dependency] private readonly StoreSystem _store = default!;
@@ -57,6 +56,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
         SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
 
+        SubscribeLocalEvent<NukeopsRoleComponent, GetBriefingEvent>(OnGetBriefing);
+
         SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
         SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
         SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
@@ -65,7 +66,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         SubscribeLocalEvent<NukeopsRuleComponent, RuleLoadedGridsEvent>(OnRuleLoadedGrids);
     }
 
-    protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
+    protected override void Started(EntityUid uid,
+        NukeopsRuleComponent component,
+        GameRuleComponent gameRule,
         GameRuleStartedEvent args)
     {
         var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
@@ -85,7 +88,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
     }
 
     #region Event Handlers
-    protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
+    protected override void AppendRoundEndText(EntityUid uid,
+        NukeopsRuleComponent component,
+        GameRuleComponent gameRule,
         ref RoundEndTextAppendEvent args)
     {
         var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
@@ -227,7 +232,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
         // If the disk is currently at Central Command, the crew wins - just slightly.
         // This also implies that some nuclear operatives have died.
-        SetWinType(ent, diskAtCentCom
+        SetWinType(ent,
+            diskAtCentCom
             ? WinType.CrewMinor
             : WinType.OpsMinor);
         ent.Comp.WinConditions.Add(diskAtCentCom
@@ -456,8 +462,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
             : WinCondition.AllNukiesDead);
 
         SetWinType(ent, WinType.CrewMajor, false);
-        _roundEndSystem.DoRoundEndBehavior(
-            nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
+        _roundEndSystem.DoRoundEndBehavior(nukeops.RoundEndBehavior,
+            nukeops.EvacShuttleTime,
+            nukeops.RoundEndTextSender,
+            nukeops.RoundEndTextShuttleCall,
+            nukeops.RoundEndTextAnnouncement);
 
         // prevent it called multiple times
         nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
@@ -465,16 +474,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
     private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
     {
-        if (ent.Comp.TargetStation is not { } station)
-            return;
+        var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target";
 
-        _antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
-                ("station", station),
+        _antag.SendBriefing(args.Session,
+            Loc.GetString("nukeops-welcome",
+                ("station", target),
                 ("name", Name(ent))),
             Color.Red,
             ent.Comp.GreetSoundNotification);
     }
 
+    private void OnGetBriefing(Entity<NukeopsRoleComponent> role, ref GetBriefingEvent args)
+    {
+        // TODO Different character screen briefing for the 3 nukie types
+        args.Append(Loc.GetString("nukeops-briefing"));
+    }
+
     /// <remarks>
     /// Is this method the shitty glue holding together the last of my sanity? yes.
     /// Do i have a better solution? not presently.
index c5f88ab6cf1e29a36f84b390c4ad4ac803e23779..939ab87115372e51f3ee5d98d7513e370e33286e 100644 (file)
@@ -15,7 +15,6 @@ using Content.Shared.Database;
 using Content.Shared.GameTicking.Components;
 using Content.Shared.Humanoid;
 using Content.Shared.IdentityManagement;
-using Content.Shared.Mind;
 using Content.Shared.Mind.Components;
 using Content.Shared.Mindshield.Components;
 using Content.Shared.Mobs;
@@ -38,8 +37,8 @@ namespace Content.Server.GameTicking.Rules;
 public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
 {
     [Dependency] private readonly IAdminLogManager _adminLogManager = default!;
-    [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly AntagSelectionSystem _antag = default!;
+    [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
     [Dependency] private readonly EuiManager _euiMan = default!;
     [Dependency] private readonly MindSystem _mind = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
@@ -49,7 +48,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
     [Dependency] private readonly SharedStunSystem _stun = default!;
     [Dependency] private readonly RoundEndSystem _roundEnd = default!;
     [Dependency] private readonly StationSystem _stationSystem = default!;
-    [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
 
     //Used in OnPostFlash, no reference to the rule component is available
     public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
@@ -59,9 +58,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
     {
         base.Initialize();
         SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
+
+        SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
         SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
+
         SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
-        SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
+
     }
 
     protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
@@ -85,7 +87,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
         }
     }
 
-    protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
+    protected override void AppendRoundEndText(EntityUid uid,
+        RevolutionaryRuleComponent component,
+        GameRuleComponent gameRule,
         ref RoundEndTextAppendEvent args)
     {
         base.AppendRoundEndText(uid, component, gameRule, ref args);
@@ -101,7 +105,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
         args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
         foreach (var (mind, data, name) in sessionData)
         {
-            var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
+            _role.MindHasRole<RevolutionaryRoleComponent>(mind, out var role);
+            var count = CompOrNull<RevolutionaryRoleComponent>(role)?.ConvertedCount ?? 0;
+
             args.AddLine(Loc.GetString("rev-headrev-name-user",
                 ("name", name),
                 ("username", data.UserName),
@@ -113,10 +119,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
 
     private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)
     {
-        if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
-            return;
-
-        var head = HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity);
+        var ent = args.Mind.Comp.OwnedEntity;
+        var head = HasComp<HeadRevolutionaryComponent>(ent);
         args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
     }
 
@@ -145,15 +149,20 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
 
         if (ev.User != null)
         {
-            _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
+            _adminLogManager.Add(LogType.Mind,
+                LogImpact.Medium,
+                $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
 
-            if (_mind.TryGetRole<RevolutionaryRoleComponent>(ev.User.Value, out var headrev))
-                headrev.ConvertedCount++;
+            if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
+            {
+                if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out _, out var role))
+                    role.Value.Comp.ConvertedCount++;
+            }
         }
 
         if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
         {
-            _role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevPrototypeId });
+            _role.MindAddRole(mindId, "MindRoleRevolutionary");
         }
 
         if (mind?.Session != null)
index 074b4c0df7a07197f9c7b5e9c2c9a090c8a24875..b00ed386363e98439d2b9f0b53a591826e287483 100644 (file)
@@ -1,18 +1,12 @@
 using Content.Server.Antag;
 using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Mind;
-using Content.Server.Objectives;
 using Content.Server.Roles;
 using Content.Shared.Humanoid;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Components;
-using Robust.Shared.Random;
 
 namespace Content.Server.GameTicking.Rules;
 
 public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
 {
-    [Dependency] private readonly MindSystem _mindSystem = default!;
     [Dependency] private readonly AntagSelectionSystem _antag = default!;
 
     public override void Initialize()
@@ -24,32 +18,33 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
         SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
     }
 
-    private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
+    // Greeting upon thief activation
+    private void AfterAntagSelected(Entity<ThiefRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
     {
-        if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
-            return;
-
-        //Generate objectives
-        _antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
+        var ent = args.EntityUid;
+        _antag.SendBriefing(ent, MakeBriefing(ent), null, null);
     }
 
-    //Add mind briefing
-    private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
+    // Character screen briefing
+    private void OnGetBriefing(Entity<ThiefRoleComponent> role, ref GetBriefingEvent args)
     {
-        if (!TryComp<MindComponent>(thief.Owner, out var mind) || mind.OwnedEntity == null)
-            return;
+        var ent = args.Mind.Comp.OwnedEntity;
 
-        args.Append(MakeBriefing(mind.OwnedEntity.Value));
+        if (ent is null)
+            return;
+        args.Append(MakeBriefing(ent.Value));
     }
 
-    private string MakeBriefing(EntityUid thief)
+    private string MakeBriefing(EntityUid ent)
     {
-        var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
+        var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
         var briefing = isHuman
             ? Loc.GetString("thief-role-greeting-human")
             : Loc.GetString("thief-role-greeting-animal");
 
-        briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
+        if (isHuman)
+            briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
+
         return briefing;
     }
 }
index 56b652ed9a07e822cdc8a324742a230a916e30bb..44ad00ae170223be294c94848f62c896b8aa3a71 100644 (file)
@@ -10,9 +10,7 @@ using Content.Shared.Database;
 using Content.Shared.GameTicking.Components;
 using Content.Shared.Mind;
 using Content.Shared.NPC.Systems;
-using Content.Shared.Objectives.Components;
 using Content.Shared.PDA;
-using Content.Shared.Radio;
 using Content.Shared.Roles;
 using Content.Shared.Roles.Jobs;
 using Content.Shared.Roles.RoleCodeword;
@@ -28,22 +26,21 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
     private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b");
 
     [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
     [Dependency] private readonly AntagSelectionSystem _antag = default!;
-    [Dependency] private readonly UplinkSystem _uplink = default!;
-    [Dependency] private readonly MindSystem _mindSystem = default!;
-    [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
     [Dependency] private readonly SharedJobSystem _jobs = default!;
+    [Dependency] private readonly MindSystem _mindSystem = default!;
+    [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
+    [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
+    [Dependency] private readonly UplinkSystem _uplink = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
-
         SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
     }
 
@@ -80,7 +77,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
 
     public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
     {
-        //Grab the mind if it wasnt provided
+        //Grab the mind if it wasn't provided
         if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
             return false;
 
@@ -92,7 +89,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         {
             // Calculate the amount of currency on the uplink.
             var startingBalance = component.StartingBalance;
-            if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
+            if (_jobs.MindTryGetJob(mindId, out var prototype))
                 startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
 
             // creadth: we need to create uplink for the antag.
@@ -111,14 +108,18 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
 
         _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
 
-
         component.TraitorMinds.Add(mindId);
 
         // Assign briefing
-        _roleSystem.MindAddRole(mindId, new RoleBriefingComponent
+        //Since this provides neither an antag/job prototype, nor antag status/roletype,
+        //and is intrinsically related to the traitor role
+        //it does not need to be a separate Mind Role Entity
+        _roleSystem.MindHasRole<TraitorRoleComponent>(mindId, out var traitorRole);
+        if (traitorRole is not null)
         {
-            Briefing = briefing
-        }, mind, true);
+            AddComp<RoleBriefingComponent>(traitorRole.Value.Owner);
+            Comp<RoleBriefingComponent>(traitorRole.Value.Owner).Briefing = briefing;
+        }
 
         // Send codewords to only the traitor client
         var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found
index e91931300e2397f6d1e69b358ba61d6368a88fc4..bb76721340dc292a6df315d92f4331a0142cc33e 100644 (file)
@@ -13,6 +13,7 @@ using Content.Shared.Mind;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
+using Content.Shared.Roles;
 using Content.Shared.Zombies;
 using Robust.Shared.Player;
 using Robust.Shared.Timing;
@@ -22,15 +23,16 @@ namespace Content.Server.GameTicking.Rules;
 
 public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 {
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly ChatSystem _chat = default!;
-    [Dependency] private readonly RoundEndSystem _roundEnd = default!;
-    [Dependency] private readonly PopupSystem _popup = default!;
-    [Dependency] private readonly MobStateSystem _mobState = default!;
-    [Dependency] private readonly ZombieSystem _zombie = default!;
     [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly PopupSystem _popup = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
+    [Dependency] private readonly RoundEndSystem _roundEnd = default!;
     [Dependency] private readonly StationSystem _station = default!;
-    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly ZombieSystem _zombie = default!;
 
     public override void Initialize()
     {
@@ -41,23 +43,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
         SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
     }
 
-    private void OnGetBriefing(EntityUid uid, InitialInfectedRoleComponent component, ref GetBriefingEvent args)
+    private void OnGetBriefing(Entity<InitialInfectedRoleComponent> role, ref GetBriefingEvent args)
     {
-        if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
-            return;
-        if (HasComp<ZombieRoleComponent>(uid)) // don't show both briefings
-            return;
-        args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
+        if (!_roles.MindHasRole<ZombieRoleComponent>(args.Mind.Owner))
+            args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
     }
 
-    private void OnGetBriefing(EntityUid uid, ZombieRoleComponent component, ref GetBriefingEvent args)
+    private void OnGetBriefing(Entity<ZombieRoleComponent> role, ref GetBriefingEvent args)
     {
-        if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
-            return;
         args.Append(Loc.GetString("zombie-infection-greeting"));
     }
 
-    protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
+    protected override void AppendRoundEndText(EntityUid uid,
+        ZombieRuleComponent component,
+        GameRuleComponent gameRule,
         ref RoundEndTextAppendEvent args)
     {
         base.AppendRoundEndText(uid, component, gameRule, ref args);
index bd276e6df70517176c0023feb010178dbfb9bee5..3dfa37a6b1392966fbe21db13c78e7715fb56ec1 100644 (file)
@@ -1,11 +1,13 @@
-namespace Content.Server.Ghost.Roles;
+using Content.Shared.Roles;
+
+namespace Content.Server.Ghost.Roles;
 
 /// <summary>
 /// This is used for round end display of ghost roles.
 /// It may also be used to ensure some ghost roles count as antagonists in future.
 /// </summary>
 [RegisterComponent]
-public sealed partial class GhostRoleMarkerRoleComponent : Component
+public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
 {
     [DataField("name")] public string? Name;
 }
index 23ce0f1539877ddcdaf95532d175fc0b221ff01f..bb95b827a7f1161cd6f802cad98326067da1f0ff 100644 (file)
@@ -71,18 +71,22 @@ public sealed class GhostRoleSystem : EntitySystem
 
         SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
         SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
+
         SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindAddedMessage>(OnMindAdded);
         SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
         SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
+        SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
+
         SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
         SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
         SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
         SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
+
         SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
         SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
+
         SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
-        SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
         SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
         SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GhostRoleRadioMessage>(OnGhostRoleRadioMessage);
         _playerManager.PlayerStatusChanged += PlayerStatusChanged;
@@ -509,7 +513,11 @@ public sealed class GhostRoleSystem : EntitySystem
 
         var newMind = _mindSystem.CreateMind(player.UserId,
             EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
-        _roleSystem.MindAddRole(newMind, new GhostRoleMarkerRoleComponent { Name = role.RoleName });
+
+        _roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
+
+        if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind, out _, out var markerRole))
+            markerRole.Value.Comp.Name = role.RoleName;
 
         _mindSystem.SetUserId(newMind, player.UserId);
         _mindSystem.TransferTo(newMind, mob);
@@ -602,10 +610,7 @@ public sealed class GhostRoleSystem : EntitySystem
 
         if (ghostRole.JobProto != null)
         {
-            if (HasComp<JobComponent>(args.Mind))
-                _roleSystem.MindRemoveRole<JobComponent>(args.Mind);
-
-            _roleSystem.MindAddRole(args.Mind, new JobComponent { Prototype = ghostRole.JobProto });
+            _roleSystem.MindAddJobRole(args.Mind, args.Mind, silent:false,ghostRole.JobProto);
         }
 
         ghostRole.Taken = true;
index d4961b82dbbc7da662f7ed8b49134e4f2a7eaa2b..c365702a3142066d2363a53a24a26babec9844ae 100644 (file)
@@ -43,7 +43,7 @@ namespace Content.Server.Mind.Commands
             builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.UserId, mind.OwnedEntity);
 
             var roles = _entities.System<SharedRoleSystem>();
-            foreach (var role in roles.MindGetAllRoles(mindId))
+            foreach (var role in roles.MindGetAllRoleInfo(mindId))
             {
                 builder.AppendFormat("{0} ", role.Name);
             }
index c1caa819e449ed2253ab157724055d6d07ec3fdc..b4de15f2b9a6cc34fda736ef231a936ad6de5dae 100644 (file)
@@ -89,7 +89,7 @@ public sealed class KillPersonConditionSystem : EntitySystem
         foreach (var mind in allHumans)
         {
             // RequireAdminNotify used as a cheap way to check for command department
-            if (_job.MindTryGetJob(mind, out _, out var prototype) && prototype.RequireAdminNotify)
+            if (_job.MindTryGetJob(mind, out var prototype) && prototype.RequireAdminNotify)
                 allHeads.Add(mind);
         }
 
index 47c54b937a070d321d7d3fd466ecf51e4ea3b8c4..ee99658f66ef12de4f0d7f9ffd25a99d40e8ab41 100644 (file)
@@ -1,9 +1,10 @@
 using Content.Server.Objectives.Components;
+using Content.Server.Roles;
 using Content.Server.Warps;
 using Content.Shared.Objectives.Components;
 using Content.Shared.Ninja.Components;
+using Content.Shared.Roles;
 using Robust.Shared.Random;
-using Content.Server.Roles;
 
 namespace Content.Server.Objectives.Systems;
 
@@ -16,6 +17,7 @@ public sealed class NinjaConditionsSystem : EntitySystem
     [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly NumberObjectiveSystem _number = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
 
     public override void Initialize()
     {
@@ -46,10 +48,8 @@ public sealed class NinjaConditionsSystem : EntitySystem
     // spider charge
     private void OnSpiderChargeRequirementCheck(EntityUid uid, SpiderChargeConditionComponent comp, ref RequirementCheckEvent args)
     {
-        if (args.Cancelled || !HasComp<NinjaRoleComponent>(args.MindId))
-        {
+        if (args.Cancelled || !_roles.MindHasRole<NinjaRoleComponent>(args.MindId))
             return;
-        }
 
         // choose spider charge detonation point
         var warps = new List<EntityUid>();
index e63492bb5ed635ccf7445bd8889dcc65b8443460..50d747c1a2a022a62585b813e35f85a868fd32d4 100644 (file)
@@ -21,7 +21,7 @@ public sealed class NotCommandRequirementSystem : EntitySystem
             return;
 
         // cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
-        if (_job.MindTryGetJob(args.MindId, out _, out var prototype) && prototype.RequireAdminNotify)
+        if (_job.MindTryGetJob(args.MindId, out var prototype) && prototype.RequireAdminNotify)
             args.Cancelled = true;
     }
 }
index 5c2b04046342db0d3c36d2d12592b1c717985fb3..ac7e579c38047e125643eff10aaa75bb3394e85e 100644 (file)
@@ -8,6 +8,8 @@ namespace Content.Server.Objectives.Systems;
 /// </summary>
 public sealed class NotJobRequirementSystem : EntitySystem
 {
+    [Dependency] private readonly SharedJobSystem _jobs = default!;
+
     public override void Initialize()
     {
         base.Initialize();
@@ -20,11 +22,10 @@ public sealed class NotJobRequirementSystem : EntitySystem
         if (args.Cancelled)
             return;
 
-        // if player has no job then don't care
-        if (!TryComp<JobComponent>(args.MindId, out var job))
-            return;
+        _jobs.MindTryGetJob(args.MindId, out var proto);
 
-        if (job.Prototype == comp.Job)
+        // if player has no job then don't care
+        if (proto is not null && proto.ID == comp.Job)
             args.Cancelled = true;
     }
 }
index 8421987bae9f5c19416aa42c7d41e3de01beac1c..83d4c2ea4c553a6f55f545a80b686709085993ef 100644 (file)
@@ -22,8 +22,6 @@ public sealed class RoleRequirementSystem : EntitySystem
         if (args.Cancelled)
             return;
 
-        // this whitelist trick only works because roles are components on the mind and not entities
-        // if that gets reworked then this will need changing
         if (_whitelistSystem.IsWhitelistFail(comp.Roles, args.MindId))
             args.Cancelled = true;
     }
index 4139499e9fbec978277ab85ecd2245fb359eef1c..0896731a2e2c278da46d1a970b0f45fc0ae1e929 100644 (file)
@@ -30,14 +30,15 @@ namespace Content.Server.Players.PlayTimeTracking;
 /// </summary>
 public sealed class PlayTimeTrackingSystem : EntitySystem
 {
+    [Dependency] private readonly IAdminManager _adminManager = default!;
     [Dependency] private readonly IAfkManager _afk = default!;
-    [Dependency] private readonly IPlayerManager _playerManager = default!;
-    [Dependency] private readonly IPrototypeManager _prototypes = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
     [Dependency] private readonly MindSystem _minds = default!;
-    [Dependency] private readonly PlayTimeTrackingManager _tracking = default!;
-    [Dependency] private readonly IAdminManager _adminManager = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
+    [Dependency] private readonly IPrototypeManager _prototypes = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
+    [Dependency] private readonly PlayTimeTrackingManager _tracking = default!;
 
     public override void Initialize()
     {
@@ -101,10 +102,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
 
     public IEnumerable<string> GetTimedRoles(EntityUid mindId)
     {
-        var ev = new MindGetAllRolesEvent(new List<RoleInfo>());
-        RaiseLocalEvent(mindId, ref ev);
-
-        foreach (var role in ev.Roles)
+        foreach (var role in _roles.MindGetAllRoleInfo(mindId))
         {
             if (string.IsNullOrWhiteSpace(role.PlayTimeTrackerId))
                 continue;
index 8e42f41cb3dde60afa8d39c7950b016427e851d7..dc16b87300e5f19772a226f4d5aae6d1580ec370 100644 (file)
@@ -10,3 +10,5 @@ public sealed partial class CommandStaffComponent : Component
 {
 
 }
+
+//TODO this should probably be on a mind role, not the mob
index b85fd53eb0cd35bf399706dc6e57c7dc1b896c1e..c47455d8f6f0534f21fbaaf13223ac60f874d4cc 100644 (file)
@@ -4,9 +4,9 @@ using Content.Shared.Roles;
 namespace Content.Server.Roles;
 
 /// <summary>
-/// Role used to keep track of space dragons for antag purposes.
+///     Added to mind role entities to tag that they are a space dragon.
 /// </summary>
-[RegisterComponent, Access(typeof(DragonSystem)), ExclusiveAntagonist]
-public sealed partial class DragonRoleComponent : AntagonistRoleComponent
+[RegisterComponent, Access(typeof(DragonSystem))]
+public sealed partial class DragonRoleComponent : BaseMindRoleComponent
 {
 }
index 52d3db41643a5d8b0e571a3b66a15a228981966e..475cd3ba603b1baad279e34fd22ee9b924ccdfe7 100644 (file)
@@ -2,8 +2,11 @@ using Content.Shared.Roles;
 
 namespace Content.Server.Roles;
 
-[RegisterComponent, ExclusiveAntagonist]
-public sealed partial class InitialInfectedRoleComponent : AntagonistRoleComponent
+/// <summary>
+///     Added to mind role entities to tag that they are an initial infected.
+/// </summary>
+[RegisterComponent]
+public sealed partial class InitialInfectedRoleComponent : BaseMindRoleComponent
 {
 
 }
index 9f0dd7ae32b5b78891ea4126697c7eab25d4e311..7e69d9d92bc4b7e57f98e47c0f9dc2af23be842a 100644 (file)
@@ -30,7 +30,7 @@ public sealed class JobSystem : SharedJobSystem
         if (!_mind.TryGetSession(mindId, out var session))
             return;
 
-        if (!MindTryGetJob(mindId, out _, out var prototype))
+        if (!MindTryGetJob(mindId, out var prototype))
             return;
 
         _chat.DispatchServerMessage(session, Loc.GetString("job-greet-introduce-job-name",
@@ -47,6 +47,6 @@ public sealed class JobSystem : SharedJobSystem
         if (MindHasJobWithId(mindId, jobPrototypeId))
             return;
 
-        _roles.MindAddRole(mindId, new JobComponent { Prototype = jobPrototypeId });
+        _roles.MindAddJobRole(mindId, null, false, jobPrototypeId);
     }
 }
index cb60e5bdf0312f79824dd0513ced01bc82894539..7bdffe67a315ac5ba928ed18f4346aa0d60d2340 100644 (file)
@@ -2,7 +2,10 @@ using Content.Shared.Roles;
 
 namespace Content.Server.Roles;
 
-[RegisterComponent, ExclusiveAntagonist]
-public sealed partial class NinjaRoleComponent : AntagonistRoleComponent
+/// <summary>
+///     Added to mind role entities to tag that they are a space ninja.
+/// </summary>
+[RegisterComponent]
+public sealed partial class NinjaRoleComponent : BaseMindRoleComponent
 {
 }
index a6ff0b71b06e96352accfe0f132bd8b729110ed7..41561088ea8d1ab3c45284d01187d087cc2cb78e 100644 (file)
@@ -3,9 +3,9 @@ using Content.Shared.Roles;
 namespace Content.Server.Roles;
 
 /// <summary>
-///     Added to mind entities to tag that they are a nuke operative.
+///     Added to mind role entities to tag that they are a nuke operative.
 /// </summary>
-[RegisterComponent, ExclusiveAntagonist]
-public sealed partial class NukeopsRoleComponent : AntagonistRoleComponent
+[RegisterComponent]
+public sealed partial class NukeopsRoleComponent : BaseMindRoleComponent
 {
 }
index feba63a253f8028066ed645e2522f367e5174a5f..fd4bb09317af5637bc6d8a1db5f71f161aea7dc2 100644 (file)
@@ -45,7 +45,7 @@ namespace Content.Server.Roles
             var roles = _entityManager.System<SharedRoleSystem>();
             var jobs = _entityManager.System<SharedJobSystem>();
             if (jobs.MindHasJobWithId(mind, args[1]))
-                roles.MindRemoveRole<JobComponent>(mind.Value);
+                roles.MindTryRemoveRole<JobRoleComponent>(mind.Value);
         }
     }
 }
index bf56b960084d06eeaf00632f7539d68afe3d328c..dcdb131b9d03213e204ce28d13cc19ff234224e0 100644 (file)
@@ -3,10 +3,10 @@ using Content.Shared.Roles;
 namespace Content.Server.Roles;
 
 /// <summary>
-///     Added to mind entities to tag that they are a Revolutionary.
+///     Added to mind role entities to tag that they are a Revolutionary.
 /// </summary>
-[RegisterComponent, ExclusiveAntagonist]
-public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent
+[RegisterComponent]
+public sealed partial class RevolutionaryRoleComponent : BaseMindRoleComponent
 {
     /// <summary>
     /// For headrevs, how many people you have converted.
index c53fa1cf9ebcc7ab1a3f3e87068efe8e6a5171e8..333f4f9b606f90023db1a549325a5b6af5a63622 100644 (file)
@@ -1,32 +1,40 @@
+using Content.Shared.Mind;
 using Content.Shared.Roles;
 
 namespace Content.Server.Roles;
 
 public sealed class RoleSystem : SharedRoleSystem
 {
-    public override void Initialize()
-    {
-        // TODO make roles entities
-        base.Initialize();
-
-        SubscribeAntagEvents<DragonRoleComponent>();
-        SubscribeAntagEvents<InitialInfectedRoleComponent>();
-        SubscribeAntagEvents<NinjaRoleComponent>();
-        SubscribeAntagEvents<NukeopsRoleComponent>();
-        SubscribeAntagEvents<RevolutionaryRoleComponent>();
-        SubscribeAntagEvents<SubvertedSiliconRoleComponent>();
-        SubscribeAntagEvents<TraitorRoleComponent>();
-        SubscribeAntagEvents<ZombieRoleComponent>();
-        SubscribeAntagEvents<ThiefRoleComponent>();
-    }
-
     public string? MindGetBriefing(EntityUid? mindId)
     {
         if (mindId == null)
+        {
+            Log.Error($"MingGetBriefing failed for mind {mindId}");
+            return null;
+        }
+
+        TryComp<MindComponent>(mindId.Value, out var mindComp);
+
+        if (mindComp is null)
+        {
+            Log.Error($"MingGetBriefing failed for mind {mindId}");
             return null;
+        }
 
         var ev = new GetBriefingEvent();
-        RaiseLocalEvent(mindId.Value, ref ev);
+
+        // This is on the event because while this Entity<T> is also present on every Mind Role Entity's MindRoleComp
+        // getting to there from a GetBriefing event subscription can be somewhat boilerplate
+        // and this needs to be looked up for the event anyway so why calculate it again later
+        ev.Mind = (mindId.Value, mindComp);
+
+        // Briefing is no longer raised on the mind entity itself
+        // because all the components that briefings subscribe to should be on Mind Role Entities
+        foreach(var role in mindComp.MindRoles)
+        {
+            RaiseLocalEvent(role, ref ev);
+        }
+
         return ev.Briefing;
     }
 }
@@ -38,8 +46,16 @@ public sealed class RoleSystem : SharedRoleSystem
 [ByRefEvent]
 public sealed class GetBriefingEvent
 {
+    /// <summary>
+    /// The text that will be shown on the Character Screen
+    /// </summary>
     public string? Briefing;
 
+    /// <summary>
+    /// The Mind to whose Mind Role Entities the briefing is sent to
+    /// </summary>
+    public Entity<MindComponent> Mind;
+
     public GetBriefingEvent(string? briefing = null)
     {
         Briefing = briefing;
index 70056fbec9e9b66450a75df0909307a60b6a8f91..55727573b9d60b869f04680d10e566e1926d7de6 100644 (file)
@@ -2,7 +2,10 @@
 
 namespace Content.Server.Roles;
 
+/// <summary>
+///     Added to mind role entities to tag that they are a hacked borg.
+/// </summary>
 [RegisterComponent]
-public sealed partial class SubvertedSiliconRoleComponent : AntagonistRoleComponent
+public sealed partial class SubvertedSiliconRoleComponent : BaseMindRoleComponent
 {
 }
index 82e350ef63082a3502ae2bbf7a6f6ef1212f27a0..c0ddee71a4988c2a22714a05a2d83cd519fcb194 100644 (file)
@@ -2,7 +2,10 @@ using Content.Shared.Roles;
 
 namespace Content.Server.Roles;
 
+/// <summary>
+///     Added to mind role entities to tag that they are a thief.
+/// </summary>
 [RegisterComponent]
-public sealed partial class ThiefRoleComponent : AntagonistRoleComponent
+public sealed partial class ThiefRoleComponent : BaseMindRoleComponent
 {
 }
index 96bfe8dd801bcae3d0328ac93e55427345e1cfbb..a8a11a8f1bd4c6808a41dc682e01ded90215046d 100644 (file)
@@ -2,7 +2,10 @@ using Content.Shared.Roles;
 
 namespace Content.Server.Roles;
 
-[RegisterComponent, ExclusiveAntagonist]
-public sealed partial class TraitorRoleComponent : AntagonistRoleComponent
+/// <summary>
+///     Added to mind role entities to tag that they are a syndicate traitor.
+/// </summary>
+[RegisterComponent]
+public sealed partial class TraitorRoleComponent : BaseMindRoleComponent
 {
 }
index 2f9948022b3a0b9127b9f81ed98552d9258b0bb2..cff25e53e8032a19d570da7f0c3bb0234a5afd2e 100644 (file)
@@ -2,7 +2,10 @@ using Content.Shared.Roles;
 
 namespace Content.Server.Roles;
 
-[RegisterComponent, ExclusiveAntagonist]
-public sealed partial class ZombieRoleComponent : AntagonistRoleComponent
+/// <summary>
+///     Added to mind role entities to tag that they are a zombie.
+/// </summary>
+[RegisterComponent]
+public sealed partial class ZombieRoleComponent : BaseMindRoleComponent
 {
 }
index 6b7df52a6ebc1de13b9a2b98fe5f5983d06f9a46..07674352ec030c92e43a0c4991aba36dce13e8f3 100644 (file)
@@ -28,12 +28,12 @@ namespace Content.Server.Silicons.Laws;
 public sealed class SiliconLawSystem : SharedSiliconLawSystem
 {
     [Dependency] private readonly IChatManager _chatManager = default!;
-    [Dependency] private readonly IPrototypeManager _prototype = default!;
     [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
     [Dependency] private readonly StationSystem _station = default!;
-    [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
     [Dependency] private readonly SharedStunSystem _stunSystem = default!;
-    [Dependency] private readonly SharedRoleSystem _roles = default!;
+    [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
@@ -178,10 +178,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
         if (component.AntagonistRole == null || !_mind.TryGetMind(uid, out var mindId, out _))
             return;
 
-        if (_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
-            return;
-
-        _roles.MindAddRole(mindId, new SubvertedSiliconRoleComponent { PrototypeId = component.AntagonistRole });
+        if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
+            _roles.MindAddRole(mindId, "MindRoleSubvertedSilicon");
     }
 
     public SiliconLawset GetLaws(EntityUid uid, SiliconLawBoundComponent? component = null)
index 69baf591bcaa277c40d78c07a58b814bd478fbc7..db82dc70a2930067636a79b62676d7ac2e6da46a 100644 (file)
@@ -12,10 +12,10 @@ namespace Content.Server.Spawners.EntitySystems;
 
 public sealed class ContainerSpawnPointSystem : EntitySystem
 {
+    [Dependency] private readonly ContainerSystem _container = default!;
     [Dependency] private readonly GameTicker _gameTicker = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
-    [Dependency] private readonly ContainerSystem _container = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly StationSystem _station = default!;
     [Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
 
@@ -32,7 +32,7 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
 
         // If it's just a spawn pref check if it's for cryo (silly).
         if (args.HumanoidCharacterProfile?.SpawnPriority != SpawnPriorityPreference.Cryosleep &&
-            (!_proto.TryIndex(args.Job?.Prototype, out var jobProto) || jobProto.JobEntity == null))
+            (!_proto.TryIndex(args.Job, out var jobProto) || jobProto.JobEntity == null))
         {
             return;
         }
@@ -49,7 +49,7 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
             if (spawnPoint.SpawnType == SpawnPointType.Unset)
             {
                 // make sure we also check the job here for various reasons.
-                if (spawnPoint.Job == null || spawnPoint.Job == args.Job?.Prototype)
+                if (spawnPoint.Job == null || spawnPoint.Job == args.Job)
                     possibleContainers.Add((uid, spawnPoint, container, xform));
                 continue;
             }
@@ -61,7 +61,7 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
 
             if (_gameTicker.RunLevel != GameRunLevel.InRound &&
                 spawnPoint.SpawnType == SpawnPointType.Job &&
-                (args.Job == null || spawnPoint.Job == args.Job.Prototype))
+                (args.Job == null || spawnPoint.Job == args.Job))
             {
                 possibleContainers.Add((uid, spawnPoint, container, xform));
             }
index be555dd54aca4dcc941bf67d9c48da3ba2604434..bd905e29824e053810f4ef37ae55ed9f338e7248 100644 (file)
@@ -39,7 +39,7 @@ public sealed class SpawnPointSystem : EntitySystem
 
             if (_gameTicker.RunLevel != GameRunLevel.InRound &&
                 spawnPoint.SpawnType == SpawnPointType.Job &&
-                (args.Job == null || spawnPoint.Job == args.Job.Prototype))
+                (args.Job == null || spawnPoint.Job == args.Job))
             {
                 possiblePositions.Add(xform.Coordinates);
             }
index e39a0943199d0e169105ba346c366ac596b40c53..3dd499537137473c11401d93c6b6e8f3a3599e6a 100644 (file)
@@ -1,4 +1,3 @@
-using System.Linq;
 using Content.Server.Access.Systems;
 using Content.Server.DetailExaminable;
 using Content.Server.Humanoid;
@@ -17,13 +16,10 @@ using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.PDA;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Content.Shared.Roles;
-using Content.Shared.Roles.Jobs;
 using Content.Shared.Station;
-using Content.Shared.StatusIcon;
 using JetBrains.Annotations;
 using Robust.Shared.Configuration;
 using Robust.Shared.Map;
@@ -41,16 +37,18 @@ namespace Content.Server.Station.Systems;
 [PublicAPI]
 public sealed class StationSpawningSystem : SharedStationSpawningSystem
 {
-    [Dependency] private readonly IConfigurationManager _configurationManager = default!;
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly SharedAccessSystem _accessSystem = default!;
     [Dependency] private readonly ActorSystem _actors = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
+    [Dependency] private readonly ArrivalsSystem _arrivalsSystem = default!;
     [Dependency] private readonly IdCardSystem _cardSystem = default!;
+    [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+    [Dependency] private readonly ContainerSpawnPointSystem _containerSpawnPointSystem = default!;
+    [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly PdaSystem _pdaSystem = default!;
-    [Dependency] private readonly SharedAccessSystem _accessSystem = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
 
     private bool _randomizeCharacters;
 
@@ -73,7 +71,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
     /// <remarks>
     /// This only spawns the character, and does none of the mind-related setup you'd need for it to be playable.
     /// </remarks>
-    public EntityUid? SpawnPlayerCharacterOnStation(EntityUid? station, JobComponent? job, HumanoidCharacterProfile? profile, StationSpawningComponent? stationSpawning = null)
+    public EntityUid? SpawnPlayerCharacterOnStation(EntityUid? station, ProtoId<JobPrototype>? job, HumanoidCharacterProfile? profile, StationSpawningComponent? stationSpawning = null)
     {
         if (station != null && !Resolve(station.Value, ref stationSpawning))
             throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
@@ -101,12 +99,12 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
     /// <returns>The spawned entity</returns>
     public EntityUid SpawnPlayerMob(
         EntityCoordinates coordinates,
-        JobComponent? job,
+        ProtoId<JobPrototype>? job,
         HumanoidCharacterProfile? profile,
         EntityUid? station,
         EntityUid? entity = null)
     {
-        _prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var prototype);
+        _prototypeManager.TryIndex(job ?? string.Empty, out var prototype);
         RoleLoadout? loadout = null;
 
         // Need to get the loadout up-front to handle names if we use an entity spawn override.
@@ -200,9 +198,9 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
         return entity.Value;
     }
 
-    private void DoJobSpecials(JobComponent? job, EntityUid entity)
+    private void DoJobSpecials(ProtoId<JobPrototype>? job, EntityUid entity)
     {
-        if (!_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out JobPrototype? prototype))
+        if (!_prototypeManager.TryIndex(job ?? string.Empty, out JobPrototype? prototype))
             return;
 
         foreach (var jobSpecial in prototype.Special)
@@ -269,7 +267,7 @@ public sealed class PlayerSpawningEvent : EntityEventArgs
     /// <summary>
     /// The job to use, if any.
     /// </summary>
-    public readonly JobComponent? Job;
+    public readonly ProtoId<JobPrototype>? Job;
     /// <summary>
     /// The profile to use, if any.
     /// </summary>
@@ -279,7 +277,7 @@ public sealed class PlayerSpawningEvent : EntityEventArgs
     /// </summary>
     public readonly EntityUid? Station;
 
-    public PlayerSpawningEvent(JobComponent? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
+    public PlayerSpawningEvent(ProtoId<JobPrototype>? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
     {
         Job = job;
         HumanoidCharacterProfile = humanoidCharacterProfile;
index 1edc4a33657b75f196ba46769c476d6752eb25ce..4b1b6013eb83cef8623a450fafe6459f0f3189a3 100644 (file)
@@ -33,16 +33,16 @@ public sealed partial class BuyerAntagCondition : ListingCondition
             return true;
 
         var roleSystem = ent.System<SharedRoleSystem>();
-        var roles = roleSystem.MindGetAllRoles(mindId);
+        var roles = roleSystem.MindGetAllRoleInfo(mindId);
 
         if (Blacklist != null)
         {
             foreach (var role in roles)
             {
-                if (role.Component is not AntagonistRoleComponent blacklistantag)
+                if (!role.Antagonist || string.IsNullOrEmpty(role.Prototype))
                     continue;
 
-                if (blacklistantag.PrototypeId != null && Blacklist.Contains(blacklistantag.PrototypeId))
+                if (Blacklist.Contains(role.Prototype))
                     return false;
             }
         }
@@ -52,10 +52,11 @@ public sealed partial class BuyerAntagCondition : ListingCondition
             var found = false;
             foreach (var role in roles)
             {
-                if (role.Component is not AntagonistRoleComponent antag)
+
+                if (!role.Antagonist || string.IsNullOrEmpty(role.Prototype))
                     continue;
 
-                if (antag.PrototypeId != null && Whitelist.Contains(antag.PrototypeId))
+                if (Whitelist.Contains(role.Prototype))
                     found = true;
             }
             if (!found)
index ea8de4a9ccd6c3604331f00ea62cb5c518b96448..43c06efad3c1d8e9d656a69387a6eeb0d20c3b0b 100644 (file)
@@ -37,13 +37,13 @@ public sealed partial class BuyerDepartmentCondition : ListingCondition
             return true;
 
         var jobs = ent.System<SharedJobSystem>();
-        jobs.MindTryGetJob(mindId, out var job, out _);
+        jobs.MindTryGetJob(mindId, out var job);
 
-        if (Blacklist != null && job?.Prototype != null)
+        if (Blacklist != null && job != null)
         {
             foreach (var department in prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
             {
-                if (department.Roles.Contains(job.Prototype.Value) && Blacklist.Contains(department.ID))
+                if (department.Roles.Contains(job.ID) && Blacklist.Contains(department.ID))
                     return false;
             }
         }
@@ -52,11 +52,11 @@ public sealed partial class BuyerDepartmentCondition : ListingCondition
         {
             var found = false;
 
-            if (job?.Prototype != null)
+            if (job != null)
             {
                 foreach (var department in prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
                 {
-                    if (department.Roles.Contains(job.Prototype.Value) && Whitelist.Contains(department.ID))
+                    if (department.Roles.Contains(job.ID) && Whitelist.Contains(department.ID))
                     {
                         found = true;
                         break;
index 6a53af188c2d992f9a4e22d8997c97057ffa072a..1ff4a97c33cea6fc57b17dbc26c49e79a1c302ea 100644 (file)
@@ -34,17 +34,17 @@ public sealed partial class BuyerJobCondition : ListingCondition
             return true;
 
         var jobs = ent.System<SharedJobSystem>();
-        jobs.MindTryGetJob(mindId, out var job, out _);
+        jobs.MindTryGetJob(mindId, out var job);
 
         if (Blacklist != null)
         {
-            if (job?.Prototype != null && Blacklist.Contains(job.Prototype))
+            if (job is not null && Blacklist.Contains(job.ID))
                 return false;
         }
 
         if (Whitelist != null)
         {
-            if (job?.Prototype == null || !Whitelist.Contains(job.Prototype))
+            if (job == null || !Whitelist.Contains(job.ID))
                 return false;
         }
 
index 84ab7e2e4a2d9251ebff438ff40aa9d74c5b6467..7acfe9dbbd9632c1754695af4701d25c04c6072d 100644 (file)
@@ -11,7 +11,6 @@ using Content.Server.Mind.Commands;
 using Content.Server.NPC;
 using Content.Server.NPC.HTN;
 using Content.Server.NPC.Systems;
-using Content.Server.Roles;
 using Content.Server.Speech.Components;
 using Content.Server.Temperature.Components;
 using Content.Shared.CombatMode;
@@ -47,18 +46,18 @@ namespace Content.Server.Zombies;
 /// </remarks>
 public sealed partial class ZombieSystem
 {
-    [Dependency] private readonly SharedHandsSystem _hands = default!;
-    [Dependency] private readonly ServerInventorySystem _inventory = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly IChatManager _chatMan = default!;
+    [Dependency] private readonly SharedCombatModeSystem _combat = default!;
     [Dependency] private readonly NpcFactionSystem _faction = default!;
-    [Dependency] private readonly NPCSystem _npc = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
-    [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
-    [Dependency] private readonly SharedCombatModeSystem _combat = default!;
-    [Dependency] private readonly IChatManager _chatMan = default!;
+    [Dependency] private readonly ServerInventorySystem _inventory = default!;
     [Dependency] private readonly MindSystem _mind = default!;
+    [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+    [Dependency] private readonly NPCSystem _npc = default!;
     [Dependency] private readonly SharedRoleSystem _roles = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
 
     /// <summary>
     /// Handles an entity turning into a zombie when they die or go into crit
@@ -234,7 +233,7 @@ public sealed partial class ZombieSystem
         if (hasMind && _mind.TryGetSession(mindId, out var session))
         {
             //Zombie role for player manifest
-            _roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
+            _roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
 
             //Greeting message for new bebe zombers
             _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
index 380d30ca143b63937a03c4bccea002c7c6a1f264..760f5026fad8674510dbb7670aaa82310dc0e3d9 100644 (file)
@@ -14,7 +14,6 @@ public sealed partial class MindContainerComponent : Component
     ///     The mind controlling this mob. Can be null.
     /// </summary>
     [DataField, AutoNetworkedField]
-    [Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
     public EntityUid? Mind { get; set; }
 
     /// <summary>
@@ -35,7 +34,6 @@ public sealed partial class MindContainerComponent : Component
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
     [DataField("ghostOnShutdown")]
-    [Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
     public bool GhostOnShutdown { get; set; } = true;
 }
 
index d603102682b7fc32f9b85b9eb7dfa486d329b114..a0812be8f74e2a0db91957be7b41a6c1d983c70d 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Shared.Actions;
 using Content.Shared.GameTicking;
 using Content.Shared.Mind.Components;
 using Robust.Shared.GameStates;
@@ -87,17 +86,21 @@ public sealed partial class MindComponent : Component
     /// <summary>
     ///     Prevents user from ghosting out
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("preventGhosting")]
+    [DataField]
     public bool PreventGhosting { get; set; }
 
     /// <summary>
     ///     Prevents user from suiciding
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("preventSuicide")]
+    [DataField]
     public bool PreventSuicide { get; set; }
 
+    /// <summary>
+    ///     Mind Role Entities belonging to this Mind
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public List<EntityUid> MindRoles = new List<EntityUid>();
+
     /// <summary>
     ///     The session of the player owning this mind.
     ///     Can be null, in which case the player is currently not logged in.
diff --git a/Content.Shared/Roles/AntagonistRoleComponent.cs b/Content.Shared/Roles/AntagonistRoleComponent.cs
deleted file mode 100644 (file)
index 748b182..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-using JetBrains.Annotations;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Roles;
-
-public abstract partial class AntagonistRoleComponent : Component
-{
-    [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
-    public string? PrototypeId;
-}
-
-/// <summary>
-/// Mark the antagonist role component as being exclusive
-/// IE by default other antagonists should refuse to select the same entity for a different antag role
-/// </summary>
-[AttributeUsage(AttributeTargets.Class, Inherited = false)]
-[BaseTypeRequired(typeof(AntagonistRoleComponent))]
-public sealed partial class ExclusiveAntagonistAttribute : Attribute
-{
-}
diff --git a/Content.Shared/Roles/Jobs/JobComponent.cs b/Content.Shared/Roles/Jobs/JobComponent.cs
deleted file mode 100644 (file)
index 7191e8b..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Roles.Jobs;
-
-/// <summary>
-///     Added to mind entities to hold the data for the player's current job.
-/// </summary>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
-public sealed partial class JobComponent : Component
-{
-    [DataField(required: true), AutoNetworkedField]
-    public ProtoId<JobPrototype>? Prototype;
-}
diff --git a/Content.Shared/Roles/Jobs/JobRoleComponent.cs b/Content.Shared/Roles/Jobs/JobRoleComponent.cs
new file mode 100644 (file)
index 0000000..dbaf12b
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Roles.Jobs;
+
+/// <summary>
+///     Added to mind role entities to mark them as a job role entity.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class JobRoleComponent : BaseMindRoleComponent
+{
+
+}
index 939a57baadff188cabb6089cc0b211ac93b2f697..94447a5af46c1cfd73e3166fdc444562acaaea4b 100644 (file)
@@ -13,8 +13,10 @@ namespace Content.Shared.Roles.Jobs;
 /// </summary>
 public abstract class SharedJobSystem : EntitySystem
 {
-    [Dependency] private readonly IPrototypeManager _prototypes = default!;
     [Dependency] private readonly SharedPlayerSystem _playerSystem = default!;
+    [Dependency] private readonly IPrototypeManager _prototypes = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
+
     private readonly Dictionary<string, string> _inverseTrackerLookup = new();
 
     public override void Initialize()
@@ -100,32 +102,44 @@ public abstract class SharedJobSystem : EntitySystem
 
     public bool MindHasJobWithId(EntityUid? mindId, string prototypeId)
     {
-        return CompOrNull<JobComponent>(mindId)?.Prototype == prototypeId;
+
+        MindRoleComponent? comp = null;
+        if (mindId is null)
+            return false;
+
+        _roles.MindHasRole<JobRoleComponent>(mindId.Value, out var role);
+
+        if (role is null)
+            return false;
+
+        comp = role.Value.Comp;
+
+        return (comp.JobPrototype == prototypeId);
     }
 
     public bool MindTryGetJob(
         [NotNullWhen(true)] EntityUid? mindId,
-        [NotNullWhen(true)] out JobComponent? comp,
         [NotNullWhen(true)] out JobPrototype? prototype)
     {
-        comp = null;
         prototype = null;
+        MindTryGetJobId(mindId, out var protoId);
 
-        return TryComp(mindId, out comp) &&
-               comp.Prototype != null &&
-               _prototypes.TryIndex(comp.Prototype, out prototype);
+        return (_prototypes.TryIndex<JobPrototype>(protoId, out prototype) || prototype is not null);
     }
 
-    public bool MindTryGetJobId([NotNullWhen(true)] EntityUid? mindId, out ProtoId<JobPrototype>? job)
+    public bool MindTryGetJobId(
+        [NotNullWhen(true)] EntityUid? mindId,
+        out ProtoId<JobPrototype>? job)
     {
-        if (!TryComp(mindId, out JobComponent? comp))
-        {
-            job = null;
+        job = null;
+
+        if (mindId is null)
             return false;
-        }
 
-        job = comp.Prototype;
-        return true;
+        if (_roles.MindHasRole<JobRoleComponent>(mindId.Value, out var role))
+            job = role.Value.Comp.JobPrototype;
+
+        return (job is not null);
     }
 
     /// <summary>
@@ -134,7 +148,7 @@ public abstract class SharedJobSystem : EntitySystem
     /// </summary>
     public bool MindTryGetJobName([NotNullWhen(true)] EntityUid? mindId, out string name)
     {
-        if (MindTryGetJob(mindId, out _, out var prototype))
+        if (MindTryGetJob(mindId, out var prototype))
         {
             name = prototype.LocalizedName;
             return true;
@@ -161,7 +175,7 @@ public abstract class SharedJobSystem : EntitySystem
         if (_playerSystem.ContentData(player) is not { Mind: { } mindId })
             return true;
 
-        if (!MindTryGetJob(mindId, out _, out var prototype))
+        if (!MindTryGetJob(mindId, out var prototype))
             return true;
 
         return prototype.CanBeAntag;
similarity index 79%
rename from Content.Shared/Roles/MindGetAllRolesEvent.cs
rename to Content.Shared/Roles/MindGetAllRoleInfoEvent.cs
index 69878739084bc7ecc65546b2ba438519c59e1f4e..a2f2820b5c3a5c6eb87522aac04e8f78da6d7811 100644 (file)
@@ -7,7 +7,7 @@ namespace Content.Shared.Roles;
 /// </summary>
 /// <param name="Roles">The list of roles on the player.</param>
 [ByRefEvent]
-public readonly record struct MindGetAllRolesEvent(List<RoleInfo> Roles);
+public readonly record struct MindGetAllRoleInfoEvent(List<RoleInfo> Roles);
 
 /// <summary>
 ///     Returned by <see cref="MindGetAllRolesEvent"/> to give some information about a player's role.
@@ -17,4 +17,4 @@ public readonly record struct MindGetAllRolesEvent(List<RoleInfo> Roles);
 /// <param name="Antagonist">Whether or not this role makes this player an antagonist.</param>
 /// <param name="PlayTimeTrackerId">The <see cref="PlayTimeTrackerPrototype"/> id associated with the role.</param>
 /// <param name="Prototype">The prototype ID of the role</param>
-public readonly record struct RoleInfo(Component Component, string Name, bool Antagonist, string? PlayTimeTrackerId, string Prototype);
+public readonly record struct RoleInfo(string Name, bool Antagonist, string? PlayTimeTrackerId, string Prototype);
diff --git a/Content.Shared/Roles/MindRoleComponent.cs b/Content.Shared/Roles/MindRoleComponent.cs
new file mode 100644 (file)
index 0000000..38b83a8
--- /dev/null
@@ -0,0 +1,48 @@
+using Content.Shared.Mind;
+using JetBrains.Annotations;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Roles;
+
+/// <summary>
+/// This holds data for, and indicates, a Mind Role entity
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class MindRoleComponent : BaseMindRoleComponent
+{
+    /// <summary>
+    ///     Marks this Mind Role as Antagonist
+    ///     A single antag Mind Role is enough to make the owner mind count as Antagonist.
+    /// </summary>
+    [DataField]
+    public bool Antag { get; set; } = false;
+
+    /// <summary>
+    ///     True if this mindrole is an exclusive antagonist. Antag setting is not checked if this is True.
+    /// </summary>
+    [DataField]
+    public bool ExclusiveAntag { get; set; } = false;
+
+    /// <summary>
+    ///     The Mind that this role belongs to
+    /// </summary>
+    public Entity<MindComponent> Mind { get; set; }
+
+    /// <summary>
+    ///     The Antagonist prototype of this role
+    /// </summary>
+    [DataField]
+    public ProtoId<AntagPrototype>? AntagPrototype { get; set; }
+
+    /// <summary>
+    ///     The Job prototype of this role
+    /// </summary>
+    [DataField]
+    public ProtoId<JobPrototype>? JobPrototype { get; set; }
+}
+
+public abstract partial class BaseMindRoleComponent : Component
+{
+
+}
index 81a360ebb7f77822ff82ae0049277f22c918c64e..cd3fe141b2be8d8ef5ca875712bc952424dcb9c7 100644 (file)
@@ -1,34 +1,31 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Administration.Logs;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
-using Content.Shared.Ghost.Roles;
+using Content.Shared.GameTicking;
 using Content.Shared.Mind;
 using Content.Shared.Roles.Jobs;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Configuration;
+using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
 
 namespace Content.Shared.Roles;
 
 public abstract class SharedRoleSystem : EntitySystem
 {
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly IPrototypeManager _prototypes = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedMindSystem _minds = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
-
-    // TODO please lord make role entities
-    private readonly HashSet<Type> _antagTypes = new();
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+    [Dependency] private readonly SharedGameTicker _gameTicker = default!;
+    [Dependency] private readonly IPrototypeManager _prototypes = default!;
 
     private JobRequirementOverridePrototype? _requirementOverride;
 
     public override void Initialize()
     {
-        // TODO make roles entities
-        SubscribeLocalEvent<JobComponent, MindGetAllRolesEvent>(OnJobGetAllRoles);
         Subs.CVar(_cfg, CCVars.GameRoleTimerOverride, SetRequirementOverride, true);
     }
 
@@ -44,62 +41,118 @@ public abstract class SharedRoleSystem : EntitySystem
             Log.Error($"Unknown JobRequirementOverridePrototype: {value}");
     }
 
-    private void OnJobGetAllRoles(EntityUid uid, JobComponent component, ref MindGetAllRolesEvent args)
+    /// <summary>
+    ///     Adds multiple mind roles to a mind
+    /// </summary>
+    /// <param name="mindId">The mind entity to add the role to</param>
+    /// <param name="roles">The list of mind roles to add</param>
+    /// <param name="mind">If the mind component is provided, it will be checked if it belongs to the mind entity</param>
+    /// <param name="silent">If true, no briefing will be generated upon receiving the mind role</param>
+    public void MindAddRoles(EntityUid mindId,
+        List<ProtoId<EntityPrototype>>? roles,
+        MindComponent? mind = null,
+        bool silent = false)
     {
-        var name = "game-ticker-unknown-role";
-        var prototype = "";
-        string? playTimeTracker = null;
-        if (component.Prototype != null && _prototypes.TryIndex(component.Prototype, out JobPrototype? job))
+        if (roles is null || roles.Count == 0)
+            return;
+
+        foreach (var proto in roles)
         {
-            name = job.Name;
-            prototype = job.ID;
-            playTimeTracker = job.PlayTimeTracker;
+            MindAddRole(mindId, proto, mind, silent);
         }
+    }
 
-        name = Loc.GetString(name);
-
-        args.Roles.Add(new RoleInfo(component, name, false, playTimeTracker, prototype));
+    /// <summary>
+    ///     Adds a mind role to a mind
+    /// </summary>
+    /// <param name="mindId">The mind entity to add the role to</param>
+    /// <param name="protoId">The mind role to add</param>
+    /// <param name="mind">If the mind component is provided, it will be checked if it belongs to the mind entity</param>
+    /// <param name="silent">If true, no briefing will be generated upon receiving the mind role</param>
+    public void MindAddRole(EntityUid mindId,
+        ProtoId<EntityPrototype> protoId,
+        MindComponent? mind = null,
+        bool silent = false)
+    {
+        if (protoId == "MindRoleJob")
+            MindAddJobRole(mindId, mind, silent, "");
+        else
+            MindAddRoleDo(mindId, protoId, mind, silent);
     }
 
-    protected void SubscribeAntagEvents<T>() where T : AntagonistRoleComponent
+    /// <summary>
+    /// Adds a Job mind role with the specified job prototype
+    /// </summary>
+    /// /// <param name="mindId">The mind entity to add the job role to</param>
+    /// <param name="mind">If the mind component is provided, it will be checked if it belongs to the mind entity</param>
+    /// <param name="silent">If true, no briefing will be generated upon receiving the mind role</param>
+    /// <param name="jobPrototype">The Job prototype for the new role</param>
+    public void MindAddJobRole(EntityUid mindId,
+        MindComponent? mind = null,
+        bool silent = false,
+        string? jobPrototype = null)
     {
-        SubscribeLocalEvent((EntityUid _, T component, ref MindGetAllRolesEvent args) =>
+        // Can't have someone get paid for two jobs now, can we
+        if (MindHasRole<JobRoleComponent>(mindId, out var jobRole)
+            && jobRole.Value.Comp.JobPrototype != jobPrototype)
         {
-            var name = "game-ticker-unknown-role";
-            var prototype = "";
-            if (component.PrototypeId != null && _prototypes.TryIndex(component.PrototypeId, out AntagPrototype? antag))
+            Resolve(mindId, ref mind);
+            if (mind is not null)
             {
-                name = antag.Name;
-                prototype = antag.ID;
+                _adminLogger.Add(LogType.Mind,
+                    LogImpact.Low,
+                    $"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp.JobPrototype}' to '{jobPrototype}'");
             }
-            name = Loc.GetString(name);
-
-            args.Roles.Add(new RoleInfo(component, name, true, null, prototype));
-        });
 
-        SubscribeLocalEvent((EntityUid _, T _, ref MindIsAntagonistEvent args) => { args.IsAntagonist = true; args.IsExclusiveAntagonist |= typeof(T).TryGetCustomAttribute<ExclusiveAntagonistAttribute>(out _); });
-        _antagTypes.Add(typeof(T));
+            jobRole.Value.Comp.JobPrototype = jobPrototype;
+        }
+        else
+            MindAddRoleDo(mindId, "MindRoleJob", mind, silent, jobPrototype);
     }
 
-    public void MindAddRoles(EntityUid mindId, ComponentRegistry components, MindComponent? mind = null, bool silent = false)
+    /// <summary>
+    ///     Creates a Mind Role
+    /// </summary>
+    private void MindAddRoleDo(EntityUid mindId,
+        ProtoId<EntityPrototype> protoId,
+        MindComponent? mind = null,
+        bool silent = false,
+        string? jobPrototype = null)
     {
         if (!Resolve(mindId, ref mind))
+        {
+            Log.Error($"Failed to add role {protoId} to mind {mindId} : Mind does not match provided mind component");
             return;
+        }
 
-        EntityManager.AddComponents(mindId, components);
         var antagonist = false;
-        foreach (var compReg in components.Values)
+
+        if (!_prototypes.TryIndex(protoId, out var protoEnt))
         {
-            var compType = compReg.Component.GetType();
+            Log.Error($"Failed to add role {protoId} to mind {mindId} : Role prototype does not exist");
+            return;
+        }
 
-            var comp = EntityManager.ComponentFactory.GetComponent(compType);
-            if (IsAntagonistRole(comp.GetType()))
-            {
-                antagonist = true;
-                break;
-            }
+        //TODO don't let a prototype being added a second time
+        //If that was somehow to occur, a second mindrole for that comp would be created
+        //Meaning any mind role checks could return wrong results, since they just return the first match they find
+
+        var mindRoleId = Spawn(protoId, MapCoordinates.Nullspace);
+        EnsureComp<MindRoleComponent>(mindRoleId);
+        var mindRoleComp = Comp<MindRoleComponent>(mindRoleId);
+
+        mindRoleComp.Mind = (mindId,mind);
+        if (jobPrototype is not null)
+        {
+            mindRoleComp.JobPrototype = jobPrototype;
+            EnsureComp<JobRoleComponent>(mindRoleId);
         }
 
+        if (mindRoleComp.Antag || mindRoleComp.ExclusiveAntag)
+            antagonist = true;
+
+        mind.MindRoles.Add(mindRoleId);
+
         var mindEv = new MindRoleAddedEvent(silent);
         RaiseLocalEvent(mindId, ref mindEv);
 
@@ -109,157 +162,330 @@ public abstract class SharedRoleSystem : EntitySystem
             RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
         }
 
-        _adminLogger.Add(LogType.Mind, LogImpact.Low,
-            $"Role components {string.Join(components.Keys.ToString(), ", ")} added to mind of {_minds.MindOwnerLoggingString(mind)}");
+        var name = Loc.GetString(protoEnt.Name);
+        if (mind.OwnedEntity is not null)
+        {
+            _adminLogger.Add(LogType.Mind,
+                LogImpact.Low,
+                $"{name} added to mind of {ToPrettyString(mind.OwnedEntity)}");
+        }
+        else
+        {
+            //TODO: This is not tied to the player on the Admin Log filters.
+            //Probably only happens when Job Role is added on initial spawn, before the mind entity is put in a mob
+            _adminLogger.Add(LogType.Mind,
+                LogImpact.Low,
+                $"{name} added to {ToPrettyString(mindId)}");
+        }
     }
 
-    public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false)
+    /// <summary>
+    ///     Removes all instances of a specific role from this mind.
+    /// </summary>
+    /// <param name="mindId">The mind to remove the role from.</param>
+    /// <typeparam name="T">The type of the role to remove.</typeparam>
+    /// <exception cref="ArgumentException">Thrown if the mind does not exist or does not have this role.</exception>
+    /// <returns>Returns False if there was something wrong with the mind or the removal. True if successful</returns>>
+    public bool MindRemoveRole<T>(EntityUid mindId) where T : IComponent
     {
-        if (!Resolve(mindId, ref mind))
-            return;
+        if (!TryComp<MindComponent>(mindId, out var mind) )
+            throw new ArgumentException($"{mindId} does not exist or does not have mind component");
 
-        if (HasComp(mindId, component.GetType()))
+        var found = false;
+        var antagonist = false;
+        var delete = new List<EntityUid>();
+        foreach (var role in mind.MindRoles)
         {
-            throw new ArgumentException($"We already have this role: {component}");
+            if (!HasComp<T>(role))
+                continue;
+
+            var roleComp = Comp<MindRoleComponent>(role);
+            antagonist = roleComp.Antag;
+            _entityManager.DeleteEntity(role);
+
+            delete.Add(role);
+            found = true;
+
         }
 
-        EntityManager.AddComponent(mindId, component);
-        var antagonist = IsAntagonistRole(component.GetType());
+        foreach (var role in delete)
+        {
+            mind.MindRoles.Remove(role);
+        }
 
-        var mindEv = new MindRoleAddedEvent(silent);
-        RaiseLocalEvent(mindId, ref mindEv);
+        if (!found)
+        {
+            throw new ArgumentException($"{mindId} does not have this role: {typeof(T)}");
+        }
+
+        var message = new RoleRemovedEvent(mindId, mind, antagonist);
 
-        var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
         if (mind.OwnedEntity != null)
         {
             RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
         }
-
-        _adminLogger.Add(LogType.Mind, LogImpact.Low,
-            $"'Role {component}' added to mind of {_minds.MindOwnerLoggingString(mind)}");
+        _adminLogger.Add(LogType.Mind,
+            LogImpact.Low,
+            $"'Role {typeof(T).Name}' removed from mind of {ToPrettyString(mind.OwnedEntity)}");
+        return true;
     }
 
     /// <summary>
-    ///     Gives this mind a new role.
+    /// Finds and removes all mind roles of a specific type
     /// </summary>
-    /// <param name="mindId">The mind to add the role to.</param>
-    /// <param name="component">The role instance to add.</param>
-    /// <typeparam name="T">The role type to add.</typeparam>
-    /// <param name="silent">Whether or not the role should be added silently</param>
-    /// <returns>The instance of the role.</returns>
-    /// <exception cref="ArgumentException">
-    ///     Thrown if we already have a role with this type.
-    /// </exception>
-    public void MindAddRole<T>(EntityUid mindId, T component, MindComponent? mind = null, bool silent = false) where T : IComponent, new()
+    /// <param name="mindId">The mind entity</param>
+    /// <typeparam name="T">The type of the role to remove.</typeparam>
+    /// <returns>True if the role existed and was removed</returns>
+    public bool MindTryRemoveRole<T>(EntityUid mindId) where T : IComponent
     {
-        if (!Resolve(mindId, ref mind))
-            return;
-
-        if (HasComp<T>(mindId))
+        if (!MindHasRole<T>(mindId))
         {
-            throw new ArgumentException($"We already have this role: {typeof(T)}");
+            Log.Warning($"Failed to remove role {typeof(T)} from {mindId} : mind does not have role ");
+            return false;
         }
 
-        AddComp(mindId, component);
-        var antagonist = IsAntagonistRole<T>();
+        if (typeof(T) == typeof(MindRoleComponent))
+            return false;
 
-        var mindEv = new MindRoleAddedEvent(silent);
-        RaiseLocalEvent(mindId, ref mindEv);
+        return MindRemoveRole<T>(mindId);
+    }
 
-        var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
-        if (mind.OwnedEntity != null)
+    /// <summary>
+    /// Finds the first mind role of a specific T type on a mind entity.
+    /// Outputs entity components for the mind role's MindRoleComponent and for T
+    /// </summary>
+    /// <param name="mindId">The mind entity</param>
+    /// <typeparam name="T">The type of the role to find.</typeparam>
+    /// <param name="role">The Mind Role entity component</param>
+    /// <param name="roleT">The Mind Role's entity component for T</param>
+    /// <returns>True if the role is found</returns>
+    public bool MindHasRole<T>(EntityUid mindId,
+        [NotNullWhen(true)] out Entity<MindRoleComponent>? role,
+        [NotNullWhen(true)] out Entity<T>? roleT) where T : IComponent
+    {
+        role = null;
+        roleT = null;
+
+        if (!TryComp<MindComponent>(mindId, out var mind))
+            return false;
+
+        var found = false;
+
+        foreach (var roleEnt in mind.MindRoles)
         {
-            RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
+            if (!HasComp<T>(roleEnt))
+                continue;
+
+            role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
+            roleT = (roleEnt,Comp<T>(roleEnt));
+            found = true;
+            break;
         }
 
-        _adminLogger.Add(LogType.Mind, LogImpact.Low,
-            $"'Role {typeof(T).Name}' added to mind of {_minds.MindOwnerLoggingString(mind)}");
+        return found;
     }
 
     /// <summary>
-    ///     Removes a role from this mind.
+    /// Finds the first mind role of a specific type on a mind entity.
+    /// Outputs an entity component for the mind role's MindRoleComponent
     /// </summary>
-    /// <param name="mindId">The mind to remove the role from.</param>
-    /// <typeparam name="T">The type of the role to remove.</typeparam>
-    /// <exception cref="ArgumentException">
-    ///     Thrown if we do not have this role.
-    /// </exception>
-    public void MindRemoveRole<T>(EntityUid mindId) where T : IComponent
+    /// <param name="mindId">The mind entity</param>
+    /// <param name="type">The Type to look for</param>
+    /// <param name="role">The output role</param>
+    /// <returns>True if the role is found</returns>
+    public bool MindHasRole(EntityUid mindId,
+        Type type,
+        [NotNullWhen(true)] out Entity<MindRoleComponent>? role)
     {
-        if (!RemComp<T>(mindId))
+        role = null;
+        // All MindRoles have this component, it would just return the first one.
+        // Order might not be what is expected.
+        // Better to report null
+        if (type == Type.GetType("MindRoleComponent"))
         {
-            throw new ArgumentException($"We do not have this role: {typeof(T)}");
+            Log.Error($"Something attempted to query mind role 'MindRoleComponent' on mind {mindId}. This component is present on every single mind role.");
+            return false;
         }
 
-        var mind = Comp<MindComponent>(mindId);
-        var antagonist = IsAntagonistRole<T>();
-        var message = new RoleRemovedEvent(mindId, mind, antagonist);
+        if (!TryComp<MindComponent>(mindId, out var mind))
+            return false;
 
-        if (mind.OwnedEntity != null)
+        var found = false;
+
+        foreach (var roleEnt in mind.MindRoles)
         {
-            RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
+            if (!HasComp(roleEnt, type))
+                continue;
+
+            role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
+            found = true;
+            break;
         }
-        _adminLogger.Add(LogType.Mind, LogImpact.Low,
-            $"'Role {typeof(T).Name}' removed from mind of {_minds.MindOwnerLoggingString(mind)}");
+
+        return found;
     }
 
-    public bool MindTryRemoveRole<T>(EntityUid mindId) where T : IComponent
+    /// <summary>
+    /// Finds the first mind role of a specific type on a mind entity.
+    /// Outputs an entity component for the mind role's MindRoleComponent
+    /// </summary>
+    /// <param name="mindId">The mind entity</param>
+    /// <param name="role">The Mind Role entity component</param>
+    /// <typeparam name="T">The type of the role to find.</typeparam>
+    /// <returns>True if the role is found</returns>
+    public bool MindHasRole<T>(EntityUid mindId,
+        [NotNullWhen(true)] out Entity<MindRoleComponent>? role) where T : IComponent
     {
-        if (!MindHasRole<T>(mindId))
-            return false;
-
-        MindRemoveRole<T>(mindId);
-        return true;
+        return MindHasRole<T>(mindId, out role, out _);
     }
 
+    /// <summary>
+    /// Finds the first mind role of a specific type on a mind entity.
+    /// </summary>
+    /// <param name="mindId">The mind entity</param>
+    /// <typeparam name="T">The type of the role to find.</typeparam>
+    /// <returns>True if the role is found</returns>
     public bool MindHasRole<T>(EntityUid mindId) where T : IComponent
     {
-        DebugTools.Assert(HasComp<MindComponent>(mindId));
-        return HasComp<T>(mindId);
+        return MindHasRole<T>(mindId, out _, out _);
     }
 
-    public List<RoleInfo> MindGetAllRoles(EntityUid mindId)
+    //TODO: Delete this later
+    /// <summary>
+    /// Returns the first mind role of a specific type
+    /// </summary>
+    /// <param name="mindId">The mind entity</param>
+    /// <returns>Entity Component of the mind role</returns>
+    [Obsolete("Use MindHasRole's output value")]
+    public Entity<MindRoleComponent>? MindGetRole<T>(EntityUid mindId) where T : IComponent
     {
-        DebugTools.Assert(HasComp<MindComponent>(mindId));
-        var ev = new MindGetAllRolesEvent(new List<RoleInfo>());
-        RaiseLocalEvent(mindId, ref ev);
-        return ev.Roles;
+        Entity<MindRoleComponent>? result = null;
+
+        var mind = Comp<MindComponent>(mindId);
+
+        foreach (var uid in mind.MindRoles)
+        {
+            if (HasComp<T>(uid) && TryComp<MindRoleComponent>(uid, out var comp))
+                result = (uid,comp);
+        }
+        return result;
     }
 
+    /// <summary>
+    /// Reads all Roles of a mind Entity and returns their data as RoleInfo
+    /// </summary>
+    /// <param name="mindId">The mind entity</param>
+    /// <returns>RoleInfo list</returns>
+    public List<RoleInfo> MindGetAllRoleInfo(EntityUid mindId)
+    {
+        var roleInfo = new List<RoleInfo>();
+
+        if (!TryComp<MindComponent>(mindId, out var mind))
+            return roleInfo;
+
+        foreach (var role in mind.MindRoles)
+        {
+            var valid = false;
+            var name = "game-ticker-unknown-role";
+            var prototype = "";
+           string? playTimeTracker = null;
+
+            var comp = Comp<MindRoleComponent>(role);
+            if (comp.AntagPrototype is not null)
+            {
+                prototype = comp.AntagPrototype;
+            }
+
+            if (comp.JobPrototype is not null && comp.AntagPrototype is null)
+            {
+                prototype = comp.JobPrototype;
+                if (_prototypes.TryIndex(comp.JobPrototype, out var job))
+                {
+                    playTimeTracker = job.PlayTimeTracker;
+                    name = job.Name;
+                    valid = true;
+                }
+                else
+                {
+                    Log.Error($" Mind Role Prototype '{role.Id}' contains invalid Job prototype: '{comp.JobPrototype}'");
+                }
+            }
+            else if (comp.AntagPrototype is not null && comp.JobPrototype is null)
+            {
+                prototype = comp.AntagPrototype;
+                if (_prototypes.TryIndex(comp.AntagPrototype, out var antag))
+                {
+                    name = antag.Name;
+                    valid = true;
+                }
+                else
+                {
+                    Log.Error($" Mind Role Prototype '{role.Id}' contains invalid Antagonist prototype: '{comp.AntagPrototype}'");
+                }
+            }
+            else if (comp.JobPrototype is not null && comp.AntagPrototype is not null)
+            {
+                Log.Error($" Mind Role Prototype '{role.Id}' contains both Job and Antagonist prototypes");
+            }
+
+            if (valid)
+                roleInfo.Add(new RoleInfo(name, comp.Antag || comp.ExclusiveAntag , playTimeTracker, prototype));
+        }
+        return roleInfo;
+    }
+
+    /// <summary>
+    /// Does this mind possess an antagonist role
+    /// </summary>
+    /// <param name="mindId">The mind entity</param>
+    /// <returns>True if the mind possesses any antag roles</returns>
     public bool MindIsAntagonist(EntityUid? mindId)
     {
-        if (mindId == null)
+        if (mindId is null)
+        {
+            Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
             return false;
+        }
 
-        DebugTools.Assert(HasComp<MindComponent>(mindId));
-        var ev = new MindIsAntagonistEvent();
-        RaiseLocalEvent(mindId.Value, ref ev);
-        return ev.IsAntagonist;
+        return CheckAntagonistStatus(mindId.Value).Item1;
     }
 
     /// <summary>
     /// Does this mind possess an exclusive antagonist role
     /// </summary>
     /// <param name="mindId">The mind entity</param>
-    /// <returns>True if the mind possesses an exclusive antag role</returns>
+    /// <returns>True if the mind possesses any exclusive antag roles</returns>
     public bool MindIsExclusiveAntagonist(EntityUid? mindId)
     {
-        if (mindId == null)
+        if (mindId is null)
+        {
+            Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
             return false;
+        }
 
-        var ev = new MindIsAntagonistEvent();
-        RaiseLocalEvent(mindId.Value, ref ev);
-        return ev.IsExclusiveAntagonist;
+        return CheckAntagonistStatus(mindId.Value).Item2;
     }
 
-    public bool IsAntagonistRole<T>()
-    {
-        return _antagTypes.Contains(typeof(T));
-    }
+   private (bool, bool) CheckAntagonistStatus(EntityUid mindId)
+   {
+       if (!TryComp<MindComponent>(mindId, out var mind))
+       {
+           Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind component not found");
+           return (false, false);
+       }
 
-    public bool IsAntagonistRole(Type component)
-    {
-        return _antagTypes.Contains(component);
+        var antagonist = false;
+        var exclusiveAntag = false;
+        foreach (var role in mind.MindRoles)
+        {
+            var roleComp = Comp<MindRoleComponent>(role);
+            if (roleComp.Antag || exclusiveAntag)
+                antagonist = true;
+            if (roleComp.ExclusiveAntag)
+                exclusiveAntag = true;
+        }
+
+        return (antagonist, exclusiveAntag);
     }
 
     /// <summary>
index 3e4e2fecb2e576dba0ae6ad1d744d3fa65b08a9b..cbe4633360f8c8b7140bc8e7cf4b0e2c068b98ce 100644 (file)
@@ -32,6 +32,12 @@ public sealed partial class EntityWhitelist
     [DataField] public string[]? Components;
     // TODO yaml validation
 
+    /// <summary>
+    ///     Mind Role Prototype names that are allowed in the whitelist.
+    /// </summary>
+    [DataField] public string[]? MindRoles;
+    // TODO yaml validation
+
     /// <summary>
     ///     Item sizes that are allowed in the whitelist.
     /// </summary>
index 57fdb523dd4b5f6444cc7b5ec8a7079b93de9b7b..7c78b410a18b416203daf30083b939d7ca6b5d93 100644 (file)
@@ -1,5 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Item;
+using Content.Shared.Roles;
 using Content.Shared.Tag;
 
 namespace Content.Shared.Whitelist;
@@ -7,6 +8,7 @@ namespace Content.Shared.Whitelist;
 public sealed class EntityWhitelistSystem : EntitySystem
 {
     [Dependency] private readonly IComponentFactory _factory = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
     [Dependency] private readonly TagSystem _tag = default!;
 
     private EntityQuery<ItemComponent> _itemQuery;
@@ -46,9 +48,30 @@ public sealed class EntityWhitelistSystem : EntitySystem
     public bool IsValid(EntityWhitelist list, EntityUid uid)
     {
         if (list.Components != null)
-            EnsureRegistrations(list);
+        {
+            var regs = StringsToRegs(list.Components);
+
+            list.Registrations ??= new List<ComponentRegistration>();
+            list.Registrations.AddRange(regs);
+        }
+
+        if (list.MindRoles != null)
+        {
+            var regs = StringsToRegs(list.MindRoles);
+
+            foreach (var role in regs)
+            {
+                if ( _roles.MindHasRole(uid, role.Type, out _))
+                {
+                    if (!list.RequireAll)
+                        return true;
+                }
+                else if (list.RequireAll)
+                    return false;
+            }
+        }
 
-        if (list.Registrations != null)
+        if (list.Registrations != null && list.Registrations.Count > 0)
         {
             foreach (var reg in list.Registrations)
             {
@@ -153,7 +176,7 @@ public sealed class EntityWhitelistSystem : EntitySystem
         return IsWhitelistPassOrNull(blacklist, uid);
     }
 
-    /// <summary>                                        
+    /// <summary>
     /// Helper function to determine if Blacklist is either null or the entity is not on the list
     /// Duplicate of equivalent Whitelist function
     /// </summary>
@@ -162,24 +185,27 @@ public sealed class EntityWhitelistSystem : EntitySystem
         return IsWhitelistFailOrNull(blacklist, uid);
     }
 
-    private void EnsureRegistrations(EntityWhitelist list)
+    private List<ComponentRegistration> StringsToRegs(string[]? input)
     {
-        if (list.Components == null)
-            return;
+        var list = new List<ComponentRegistration>();
 
-        list.Registrations = new List<ComponentRegistration>();
-        foreach (var name in list.Components)
+        if (input == null || input.Length == 0)
+            return list;
+
+        foreach (var name in input)
         {
             var availability = _factory.GetComponentAvailability(name);
             if (_factory.TryGetRegistration(name, out var registration)
                 && availability == ComponentAvailability.Available)
             {
-                list.Registrations.Add(registration);
+                list.Add(registration);
             }
             else if (availability == ComponentAvailability.Unknown)
             {
-                Log.Warning($"Unknown component name {name} passed to EntityWhitelist!");
+                Log.Error($"StringsToRegs failed: Unknown component name {name} passed to EntityWhitelist!");
             }
         }
+
+        return list;
     }
 }
index 1a4fcafbf862d61178eec8323e4ef6114ce9cd1d..06f8aeb565e56ec74d1ad34bfcb86ed2cc8abb8b 100644 (file)
@@ -4,6 +4,7 @@ nukeops-description = Nuclear operatives have targeted the station. Try to keep
 nukeops-welcome =
     You are a nuclear operative. Your goal is to blow up {$station}, and ensure that it is nothing but a pile of rubble. Your bosses, the Syndicate, have provided you with the tools you'll need for the task.
     Operation {$name} is a go ! Death to Nanotrasen!
+nukeops-briefing = Your objectives are simple. Deliver the payload and make sure it detonates. Begin mission.
 
 nukeops-opsmajor = [color=crimson]Syndicate major victory![/color]
 nukeops-opsminor = [color=crimson]Syndicate minor victory![/color]
index dc44915f53da54ff9b238ab46cdb4c77dd289b41..e5e1192fc686ae045519d1f3f4e52b80e69201b2 100644 (file)
       min: 1
       max: 1
       pickPlayer: false
-      mindComponents:
-      - type: DragonRole
-        prototype: Dragon
+      mindRoles:
+      - MindRoleDragon
 
 - type: entity
   parent: BaseGameRule
         nameSegments:
         - names_ninja_title
         - names_ninja
-      mindComponents:
-      - type: NinjaRole
-        prototype: SpaceNinja
+      mindRoles:
+      - MindRoleNinja
 
 - type: entity
   parent: BaseGameRule
       - type: ZombifyOnDeath
       - type: IncurableZombie
       - type: InitialInfected
-      mindComponents:
-      - type: InitialInfectedRole
-        prototype: InitialInfected
+      mindRoles:
+      - MindRoleInitialInfected
 
 - type: entity
   parent: BaseNukeopsRule
       startingGear: SyndicateLoneOperativeGearFull
       roleLoadout:
       - RoleSurvivalNukie
-
       components:
       - type: NukeOperative
       - type: RandomMetadata
       - type: NpcFactionMember
         factions:
         - Syndicate
-      mindComponents:
-      - type: NukeopsRole
-        prototype: Nukeops
+      mindRoles:
+      - MindRoleNukeops
 
 - type: entity
   parent: BaseTraitorRule
       blacklist:
         components:
         - AntagImmune
-      mindComponents:
-      - type: TraitorRole
-        prototype: TraitorSleeper
+      mindRoles:
+      - MindRoleTraitorSleeper
 
 - type: entity
   id: MassHallucinations
index 7446995f2629ea78dbfbbadc6fab87aed4039ddd..6cc53a3d100d378331e660ea5639d80c4a38473f 100644 (file)
@@ -26,8 +26,7 @@
       startingGear: ThiefGear
       components:
       - type: Pacified
-      mindComponents:
-      - type: ThiefRole
-        prototype: Thief
+      mindRoles:
+      - MindRoleThief
       briefing:
         sound: "/Audio/Misc/thief_greeting.ogg"
index caf2ca7945c49d2659405af7548cc1f06a5d9370..46d4366f680b2fa824c6d92c620dde48cce7d00b 100644 (file)
       - type: NpcFactionMember
         factions:
         - Syndicate
-      mindComponents:
-      - type: NukeopsRole
-        prototype: NukeopsCommander
+      mindRoles:
+      - MindRoleNukeopsCommander
     - prefRoles: [ NukeopsMedic ]
       fallbackRoles: [ Nukeops, NukeopsCommander ]
       spawnerPrototype: SpawnPointNukeopsMedic
       - type: NpcFactionMember
         factions:
         - Syndicate
-      mindComponents:
-      - type: NukeopsRole
-        prototype: NukeopsMedic
+      mindRoles:
+      - MindRoleNukeopsMedic
     - prefRoles: [ Nukeops ]
       fallbackRoles: [ NukeopsCommander, NukeopsMedic ]
       spawnerPrototype: SpawnPointNukeopsOperative
       - type: NpcFactionMember
         factions:
         - Syndicate
-      mindComponents:
-      - type: NukeopsRole
-        prototype: Nukeops
+      mindRoles:
+      - MindRoleNukeops
 
 - type: entity
   abstract: true
         components:
         - AntagImmune
       lateJoinAdditional: true
-      mindComponents:
-      - type: TraitorRole
-        prototype: Traitor
+      mindRoles:
+      - MindRoleTraitor
 
 - type: entity
   id: Revolutionary
       components:
       - type: Revolutionary
       - type: HeadRevolutionary
-      mindComponents:
-      - type: RevolutionaryRole
-        prototype: HeadRev
+      mindRoles:
+      - MindRoleHeadRevolutionary
 
 - type: entity
   id: Sandbox
       - type: ZombifyOnDeath
       - type: IncurableZombie
       - type: InitialInfected
-      mindComponents:
-      - type: InitialInfectedRole
-        prototype: InitialInfected
+      mindRoles:
+      - MindRoleInitialInfected
 
 # event schedulers
 
index bbdac8faa1a9a0386e41d728aac33b9e9fef44bf..9d81c62ab3f43b4490d750e3afd3aabf528bc71a 100644 (file)
@@ -9,7 +9,7 @@
     issuer: objective-issuer-dragon
   - type: RoleRequirement
     roles:
-      components:
+      mindRoles:
       - DragonRole
 
 - type: entity
index 4d0cf6c17c83b0f0695e3d02754ed41511a42bdd..28b624519c49f6108e4283bddb69185056575a7a 100644 (file)
@@ -9,7 +9,7 @@
     issuer: objective-issuer-spiderclan
   - type: RoleRequirement
     roles:
-      components:
+      mindRoles:
       - NinjaRole
 
 - type: entity
index 2dbb2cc33956b764881d473102629269a093d687..c36b5f3192126744830db24d108367912e12c9bb 100644 (file)
@@ -7,7 +7,7 @@
     issuer: objective-issuer-thief
   - type: RoleRequirement
     roles:
-      components:
+      mindRoles:
       - ThiefRole
 
 - type: entity
index a156061a9aeb876571e4c1fe1c9e28d1c6bb5848..b6f31449c02890e238d8d68d22e8ed2541ab1c01 100644 (file)
@@ -7,7 +7,7 @@
     issuer: objective-issuer-syndicate
   - type: RoleRequirement
     roles:
-      components:
+      mindRoles:
       - TraitorRole
 
 - type: entity
diff --git a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml
new file mode 100644 (file)
index 0000000..eb92fa5
--- /dev/null
@@ -0,0 +1,180 @@
+- type: entity
+  id: BaseMindRole
+  name: Mind Role
+  description: Mind Role entity
+  abstract: true
+  components:
+  - type: MindRole
+
+- type: entity
+  parent: BaseMindRole
+  id: BaseMindRoleAntag
+  abstract: true
+  components:
+  - type: MindRole
+    antag: true
+
+#Observer
+- type: entity
+  parent: BaseMindRole
+  id: MindRoleObserver
+  name: Observer Role
+  components:
+  - type: ObserverRole
+
+#Ghostrole Marker
+- type: entity
+  parent: BaseMindRole
+  id: MindRoleGhostMarker
+  name: Ghost Role
+  components:
+  - type: GhostRoleMarkerRole
+
+# The Job MindRole holds the mob's Job prototype
+- type: entity
+  parent: BaseMindRole
+  id: MindRoleJob
+  name: Job Role
+#  description:
+  # MindRoleComponent.JobPrototype is filled by SharedJobSystem
+
+# Subverted Silicon
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleSubvertedSilicon
+  name: Subverted Silicon Role
+  description:
+  components:
+  - type: SubvertedSiliconRole
+
+# Dragon
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleDragon
+  name: Dragon Role
+#  description:
+  components:
+  - type: MindRole
+    antagPrototype: Dragon
+    exclusiveAntag: true
+  - type: DragonRole
+  - type: RoleBriefing
+    briefing: dragon-role-briefing
+
+# Ninja
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleNinja
+  name: Space Ninja Role
+#  description: mind-role-ninja-description
+  components:
+  - type: MindRole
+    antagPrototype: SpaceNinja
+    exclusiveAntag: true
+  - type: NinjaRole
+
+# Nukies
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleNukeops
+  name: Nukeops Operative Role
+#  description: mind-role-nukeops-description
+  components:
+  - type: MindRole
+    exclusiveAntag: true
+    antagPrototype: Nukeops
+  - type: NukeopsRole
+
+- type: entity
+  parent: MindRoleNukeops
+  id: MindRoleNukeopsMedic
+  name: Nukeops Medic Role
+#  description: mind-role-nukeops-medic-description
+  components:
+  - type: MindRole
+    antagPrototype: NukeopsMedic
+
+- type: entity
+  parent: MindRoleNukeops
+  id: MindRoleNukeopsCommander
+  name: Nukeops Commander Role
+#  description: mind-role-nukeops-commander-description
+  components:
+  - type: MindRole
+    antagPrototype: NukeopsCommander
+
+# Revolutionaries
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleHeadRevolutionary
+  name: Head Revolutionary Role
+#  description: mind-role-head-revolutionary-description
+  components:
+  - type: MindRole
+    antagPrototype: HeadRev
+    exclusiveAntag: true
+  - type: RevolutionaryRole
+
+- type: entity
+  parent: MindRoleHeadRevolutionary
+  id: MindRoleRevolutionary
+  name: Revolutionary Role
+#  description: mind-role-revolutionary-description
+  components:
+  - type: MindRole
+    antagPrototype: Rev
+
+# Thief
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleThief
+  name: Thief Role
+#  description: mind-role-thief-description
+  components:
+  - type: MindRole
+    antagPrototype: Thief
+  - type: ThiefRole
+
+# Traitors
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleTraitor
+  name: Traitor Role
+#  description: mind-role-traitor-description
+  components:
+  - type: MindRole
+    antagPrototype: Traitor
+    exclusiveAntag: true
+  - type: TraitorRole
+
+- type: entity
+  parent: MindRoleTraitor
+  id: MindRoleTraitorSleeper
+  name: Sleeper Agent Role
+#  description: mind-role-traitor-sleeper-description
+  components:
+  - type: MindRole
+    antagPrototype: TraitorSleeper
+
+# Zombie Squad
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleInitialInfected
+  name: Initial Infected Role
+#  description: mind-role-initial-infected-description
+  components:
+  - type: MindRole
+    antagPrototype: InitialInfected
+    exclusiveAntag: true
+  - type: InitialInfectedRole
+
+- type: entity
+  parent: BaseMindRoleAntag
+  id: MindRoleZombie
+  name: Zombie Role
+#  description: mind-role-zombie-description
+  components:
+  - type: MindRole
+    antagPrototype: Zombie
+    exclusiveAntag: true
+  - type: ZombieRole