From 3df223751828b0a754c643e275e8a8ca29413d55 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Mon, 16 Mar 2026 12:05:21 -0500
Subject: [PATCH 01/54] add IPlayerRequirement and requirement context
---
.../PlayerRequirementManager.Context.cs | 22 ++++++
.../PlayerRequirements/IPlayerRequirement.cs | 67 +++++++++++++++++++
2 files changed, 89 insertions(+)
create mode 100644 Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs
create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs
new file mode 100644
index 0000000000..1737ea121f
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs
@@ -0,0 +1,22 @@
+using Content.Shared.Players.PlayTimeTracking;
+using Content.Shared.Preferences;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._DEN.Requirements.Managers;
+
+///
+/// A record that represents a list of factors that will be checked against PlayerRequirements.
+/// All fields are nullable. A null field represents an optional parameter.
+///
+public partial record PlayerRequirementContext
+{
+ ///
+ /// The currently-selected character profile of the player we are going to check.
+ ///
+ public HumanoidCharacterProfile? Profile = null;
+
+ ///
+ /// A dictionary of registered playtimes for the player.
+ ///
+ public Dictionary, TimeSpan>? Playtimes = null;
+}
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
new file mode 100644
index 0000000000..14a99088db
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -0,0 +1,67 @@
+using Content.Shared._DEN.Requirements.Managers;
+
+namespace Content.Shared._DEN.Requirements.PlayerRequirements;
+
+///
+/// This interface is used to check some parameters associated with a player (a
+/// ) to determine whether or not the player is allowed to
+/// use a certain feature.
+///
+///
+/// This is a replacement of the old JobRequirement system.
+///
+[ImplicitDataDefinitionForInheritors]
+public partial interface IPlayerRequirement
+{
+ ///
+ /// Whether or not this requirement is inverted.
+ /// In all cases where this requirement should fail, it will succeed instead, and vice versa.
+ ///
+ [DataField]
+ bool Inverted { get; set; }
+
+ ///
+ /// Requirements undergo a "PreCheck" to check if the context value has all relevant parameters.
+ /// If this is false, then the requirement auto-passes the requirement if the pre-check fails.
+ ///
+ ///
+ /// Say you have a requirement that checks your playtimes. In the pre-check, we check if the
+ /// context's playtimes are non-null. If your playtimes are null, we fail the pre-check.
+ ///
+ /// If MustPassPreCheck is false, then this will treat it as auto-passing the requirement;
+ /// we ignore the requirement entirely. If MustPassPreCheck is true, we auto-fail the
+ /// requirement instead.
+ ///
+ [DataField]
+ bool MustPassPreCheck { get; set; }
+
+ ///
+ /// Check if the given context has all parameters required in order to perform an actual check.
+ ///
+ ///
+ /// Because context parameters are meant to be optional, we can use this information to ignore
+ /// (auto-succeed) checks where we lack all the needded parameters, depending on the value of
+ /// .
+ ///
+ /// A definition of parameters to check against the requirement.
+ ///
+ bool PreCheck(PlayerRequirementContext context);
+
+ ///
+ /// Check a context's fields against this requirement to determine if it passes or fails.
+ ///
+ /// A definition of parameters to check against the requirement.
+ /// Whether or not the given context passes this requirement.
+ bool CheckRequirement(PlayerRequirementContext context);
+
+ ///
+ /// Get a localized string representing how this requirement displays to players.
+ ///
+ ///
+ /// This should, ideally, display regardless if the context actually passes or not.
+ /// It should also display differently depending on the value of .
+ ///
+ /// A definition of parameters to check against the requirement.
+ /// An optional string representing the requirement text to display to a player.
+ string? GetReason(PlayerRequirementContext context);
+}
From d4c5754a6be68905721bfc11f558cab94e41d278 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 17 Mar 2026 12:37:16 -0500
Subject: [PATCH 02/54] add player requirement manager
---
.../Managers/PlayerRequirementManager.cs | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
new file mode 100644
index 0000000000..6e01650a7e
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -0,0 +1,37 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
+using JetBrains.Annotations;
+
+namespace Content.Shared._DEN.Requirements.Managers;
+
+public sealed partial class PlayerRequirementManager
+{
+ ///
+ /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements.
+ ///
+ /// The context containing fields to check against the requirements.
+ /// An enumerable collection of requirements.
+ /// Whether or not this context passes *all* requirements. If even one fails, then this is false.
+ [PublicAPI]
+ public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements)
+ {
+ foreach (var requirement in requirements)
+ {
+ // Pre-check the actual requirement. This ensures our context has all the fields needed for the requirement.
+ // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck.
+ // If you don't need to pass the pre-check, then it's an auto-success.
+ if (!requirement.PreCheck(context))
+ {
+ if (requirement.MustPassPreCheck)
+ return false;
+
+ continue;
+ }
+
+ // Check the actual requirement, now.
+ if (!requirement.CheckRequirement(context))
+ return false;
+ }
+
+ return true;
+ }
+}
From d2a932931b8c3d0b3e02a7c83efc327deac98c75 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 17 Mar 2026 12:37:25 -0500
Subject: [PATCH 03/54] add department playtime requirements
---
.../PlayerRequirements.Playtime.cs | 174 ++++++++++++++++++
.../requirements/playtime-requirements.ftl | 6 +
2 files changed, 180 insertions(+)
create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
create mode 100644 Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
new file mode 100644
index 0000000000..9ccb893f1d
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -0,0 +1,174 @@
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._DEN.Requirements.PlayerRequirements;
+
+///
+/// An abstract class for playtime requirements that expect a playtime to be within
+/// optional minimum and maximum parameters.
+///
+public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement
+{
+ ///
+ [DataField] public bool Inverted { get; set; } = false;
+
+ ///
+ [DataField] public bool MustPassPreCheck { get; set; } = false;
+
+ ///
+ /// The minimum time you can have in this tracker.
+ ///
+ [DataField] public TimeSpan? MinTime = null;
+
+ ///
+ /// The maximum time you can have in this tracker.
+ ///
+ [DataField] public TimeSpan? MaxTime = null;
+
+ ///
+ public bool PreCheck(PlayerRequirementContext context)
+ {
+ return context.Playtimes != null;
+ }
+
+ ///
+ public abstract bool CheckRequirement(PlayerRequirementContext context);
+
+ ///
+ public abstract string? GetReason(PlayerRequirementContext context);
+
+ ///
+ /// Check if a given playtime tracker fits within the minimum and maximum times of this requirement.
+ ///
+ /// The playtime to check.
+ /// Whether or not this playtime is valid.
+ protected bool IsValidPlaytime(TimeSpan playtime)
+ {
+ if (MinTime != null & playtime < MinTime)
+ return false;
+
+ if (MaxTime != null & playtime > MaxTime)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Gets a localized "reason" string for this requirement's playtime ranges.
+ ///
+ ///
+ /// For example: "Less than 30 minutes", "At least 120 minutes", "Between 20 minutes and 180 minutes".
+ ///
+ ///
+ /// A string describing how much playtime you should have. Null if both minimum and maximum as null.
+ ///
+ protected string? GetPlaytimeConstraintReason()
+ {
+ if (MinTime == null && MaxTime == null)
+ return null;
+
+ var minTimeString = FormatPlaytime(MinTime);
+ var maxTimeString = FormatPlaytime(MaxTime);
+
+ return (MinTime, MaxTime) switch
+ {
+ (not null, not null) => Loc.GetString("player-requirement-playtime-minmax-time",
+ ("minimum", minTimeString), ("maximum", maxTimeString)),
+
+ (null, not null) => Loc.GetString("player-requirement-playtime-maximum-time",
+ ("playtime", maxTimeString)),
+
+ (not null, null) => Loc.GetString("player-requirement-playtime-minimum-time",
+ ("playtime", minTimeString)),
+
+ _ => null
+ };
+ }
+
+ ///
+ /// Gets a localized time string for the given playtime.
+ ///
+ /// The playtime to format.
+ /// A localized time string for this playtime.
+ private static string FormatPlaytime(TimeSpan? playtime)
+ {
+ var time = ((int?)playtime?.TotalMinutes) ?? 0;
+
+ return playtime != null
+ ? Loc.GetString("player-requirement-playtime-time", ("playtime", time))
+ : string.Empty;
+ }
+}
+
+///
+/// Checks if a player's total playtime in a given department fits within a given playtime range.
+///
+public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytimeRequirement
+{
+ ///
+ /// The department we should check against the requirement.
+ ///
+ [DataField(required: true)]
+ public ProtoId Department = default!;
+
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ var playtime = GetDepartmentPlaytime(context);
+ if (playtime is null)
+ return false;
+
+ return IsValidPlaytime(playtime.Value);
+ }
+
+ ///
+ public override string? GetReason(PlayerRequirementContext context)
+ {
+ // Get the department name.
+ var protoMan = IoCManager.Resolve();
+ if (!protoMan.TryIndex(Department, out var department))
+ return null;
+
+ var deptName = Loc.GetString(department.Name);
+
+ // Get the playtime constraint string.
+ var playtimeString = GetPlaytimeConstraintReason();
+ if (playtimeString == null)
+ return null;
+
+ // E.g. "You must have 120 minutes in the Science department."
+ return Loc.GetString("player-requirement-department-playtime-reason",
+ ("timeConstraint", playtimeString),
+ ("department", deptName));
+ }
+
+ ///
+ /// Get the total playtime for this department.
+ ///
+ /// A definition of parameters to check against the requirement.
+ /// The total playtime of this department. Null if either context playtimes or department is invalid.
+ private TimeSpan? GetDepartmentPlaytime(PlayerRequirementContext context)
+ {
+ var protoMan = IoCManager.Resolve();
+ var playtime = TimeSpan.Zero;
+
+ if (context.Playtimes == null
+ || !protoMan.TryIndex(Department, out var department))
+ return null;
+
+ // Sum the playtimes of all roles in this department.
+ foreach (var roleId in department.Roles)
+ {
+ if (!protoMan.TryIndex(roleId, out var role))
+ continue;
+
+ if (!context.Playtimes.TryGetValue(role.PlayTimeTracker, out var roleTime))
+ continue;
+
+ playtime += roleTime;
+ }
+
+ return playtime;
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
new file mode 100644
index 0000000000..4da5160306
--- /dev/null
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -0,0 +1,6 @@
+player-requirement-playtime-time = {$playtime} minutes
+player-requirement-playtime-minimum-time = at least {$playtime}
+player-requirement-playtime-maximum-time = less than {$playtime}
+player-requirement-playtime-minmax-time = between {$minimum} and {$maximum}
+
+player-requirement-department-playtime-reason = Must have {$timeConstraint} in the {$department} department.
From 3b5240aced798c8c03794ce9296a026680e5dd51 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 17 Mar 2026 13:13:21 -0500
Subject: [PATCH 04/54] redundancy
---
.../_DEN/Requirements/Managers/PlayerRequirementManager.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
index 6e01650a7e..8eafb3d728 100644
--- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -16,7 +16,7 @@ public static bool CheckRequirements(PlayerRequirementContext context, IEnumerab
{
foreach (var requirement in requirements)
{
- // Pre-check the actual requirement. This ensures our context has all the fields needed for the requirement.
+ // Pre-check the requirement. This ensures our context has all the fields needed for the requirement.
// If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck.
// If you don't need to pass the pre-check, then it's an auto-success.
if (!requirement.PreCheck(context))
From d6253d5c9ad9e1b0f12ccdb13f33dd6931b98cd3 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 13:18:47 -0500
Subject: [PATCH 05/54] add abstract player requirement class
---
.../PlayerRequirements/IPlayerRequirement.cs | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 14a99088db..2458beefec 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -65,3 +65,24 @@ public partial interface IPlayerRequirement
/// An optional string representing the requirement text to display to a player.
string? GetReason(PlayerRequirementContext context);
}
+
+///
+/// Abstract class inherited by other player requirements.
+///
+public abstract partial class PlayerRequirement : IPlayerRequirement
+{
+ ///
+ [DataField] public bool Inverted { get; set; } = false;
+
+ ///
+ [DataField] public bool MustPassPreCheck { get; set; } = false;
+
+ ///
+ public abstract bool CheckRequirement(PlayerRequirementContext context);
+
+ ///
+ public abstract string? GetReason(PlayerRequirementContext context);
+
+ ///
+ public abstract bool PreCheck(PlayerRequirementContext context);
+}
From 651a20f13bd433a50694a14266766952645c65d5 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 13:19:00 -0500
Subject: [PATCH 06/54] add inverted string to playtime reasons
---
.../PlayerRequirements.Playtime.cs | 22 ++++++-------------
.../requirements/playtime-requirements.ftl | 6 ++++-
2 files changed, 12 insertions(+), 16 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 9ccb893f1d..b51b7cb060 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -8,14 +8,8 @@ namespace Content.Shared._DEN.Requirements.PlayerRequirements;
/// An abstract class for playtime requirements that expect a playtime to be within
/// optional minimum and maximum parameters.
///
-public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement
+public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement
{
- ///
- [DataField] public bool Inverted { get; set; } = false;
-
- ///
- [DataField] public bool MustPassPreCheck { get; set; } = false;
-
///
/// The minimum time you can have in this tracker.
///
@@ -27,17 +21,11 @@ public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement
[DataField] public TimeSpan? MaxTime = null;
///
- public bool PreCheck(PlayerRequirementContext context)
+ public override bool PreCheck(PlayerRequirementContext context)
{
return context.Playtimes != null;
}
- ///
- public abstract bool CheckRequirement(PlayerRequirementContext context);
-
- ///
- public abstract string? GetReason(PlayerRequirementContext context);
-
///
/// Check if a given playtime tracker fits within the minimum and maximum times of this requirement.
///
@@ -137,9 +125,13 @@ public override bool CheckRequirement(PlayerRequirementContext context)
if (playtimeString == null)
return null;
+ var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
+ ("inverted", Inverted),
+ ("timeConstraint", playtimeString));
+
// E.g. "You must have 120 minutes in the Science department."
return Loc.GetString("player-requirement-department-playtime-reason",
- ("timeConstraint", playtimeString),
+ ("constraint", constraintReason),
("department", deptName));
}
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
index 4da5160306..70f4ad6099 100644
--- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -2,5 +2,9 @@ player-requirement-playtime-time = {$playtime} minutes
player-requirement-playtime-minimum-time = at least {$playtime}
player-requirement-playtime-maximum-time = less than {$playtime}
player-requirement-playtime-minmax-time = between {$minimum} and {$maximum}
+player-requirement-playtime-constraint-reason = Must {$inverted ->
+ [true] not
+ *[false] {""}
+} have {$timeConstraint}
-player-requirement-department-playtime-reason = Must have {$timeConstraint} in the {$department} department.
+player-requirement-department-playtime-reason = {$constraint} in the {$department} department.
From 58876e30c4b20a6ad6830bc4ad30810d37749896 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:04:53 -0500
Subject: [PATCH 07/54] Add count requirements
---
.../PlayerRequirements/CountRequirement.cs | 137 ++++++++++++++++++
.../_DEN/requirements/requirement-range.ftl | 5 +
2 files changed, 142 insertions(+)
create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
create mode 100644 Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
new file mode 100644
index 0000000000..9a3b3df26a
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -0,0 +1,137 @@
+using System.Linq;
+using JetBrains.Annotations;
+
+namespace Content.Shared._DEN.Requirements.PlayerRequirements;
+
+///
+/// An abstract class for requirements to determine how many items in a set should be selected.
+///
+///
+/// For example: A player selecting multiple traits that overlap with a certain required group of traits.
+///
+[ImplicitDataDefinitionForInheritors]
+[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
+public abstract partial class CountRequirement
+{
+ ///
+ /// Gets a string reason representation for this range.
+ ///
+ ///
+ /// This would slot into the sentence "You must have [reason] of the following items: [items]".
+ ///
+ /// "At least 1", "between 2 and 5", "all"
+ /// A string representation of this range's requirement bounds.
+ public abstract string GetReason();
+
+ ///
+ /// Check if our currently-selected items in a collection meets this
+ /// requirement, against a collection of required items.
+ ///
+ /// The type of the collection.
+ /// The items we have selected.
+ /// The items required for this condition.
+ /// Whether or not we fulfill this requirement.
+ public abstract bool CheckRequirement(IEnumerable have, IEnumerable required);
+
+ ///
+ /// Get how many of the required items in a collection we currently have.
+ ///
+ /// The type of the collection.
+ /// The items we have selected.
+ /// The items required for this condition.
+ /// How many of the required items we have.
+ protected static int GetFulfilledCount(IEnumerable have, IEnumerable required)
+ {
+ return have.Intersect(required).Count();
+ }
+}
+
+///
+/// To fulfill this requirement, you must have exactly some number of items.
+///
+public sealed partial class ConstantCountRequirement : CountRequirement
+{
+ ///
+ /// How many items in the collection you need to pass the requirement.
+ ///
+ [DataField]
+ public int Count;
+
+ ///
+ public override string GetReason()
+ {
+ return Loc.GetString("count-requirement-constant-reason",
+ ("count", Count));
+ }
+
+ ///
+ public override bool CheckRequirement(IEnumerable have, IEnumerable required)
+ {
+ var count = GetFulfilledCount(have, required);
+ return count == Count;
+ }
+}
+
+///
+/// To fulfill this requirement, you must have a number of items between two values, or either a minimum / maximum.
+///
+public sealed partial class RangeCountRequirement : CountRequirement
+{
+ ///
+ /// Minimum amount of required items you need.
+ ///
+ [DataField]
+ public int? Min = null;
+
+ ///
+ /// Maximum amount of required items you can have.
+ ///
+ [DataField]
+ public int? Max = null;
+
+ ///
+ public override string GetReason()
+ {
+ return (Min, Max) switch
+ {
+ (not null, not null) => Loc.GetString("count-requirement-range-minmax-reason",
+ ("minimum", Min),
+ ("maximum", Max)),
+
+ (null, not null) => Loc.GetString("count-requirement-range-maximum-reason",
+ ("maximum", Max)),
+
+ (not null, null) => Loc.GetString("count-requirement-range-minimum-reason",
+ ("minimum", Min)),
+
+ _ => string.Empty
+ };
+ }
+
+ ///
+ public override bool CheckRequirement(IEnumerable have, IEnumerable required)
+ {
+ var count = GetFulfilledCount(have, required);
+ return (Min == null || count >= Min)
+ && (Max == null || count <= Max);
+ }
+}
+
+///
+/// To fulfill the requirement, you must have all items in the required collection.
+///
+public sealed partial class AllCountRequirement : CountRequirement
+{
+ ///
+ public override string GetReason()
+ {
+ return Loc.GetString("count-requirement-all-reason");
+ }
+
+ ///
+ public override bool CheckRequirement(IEnumerable have, IEnumerable required)
+ {
+ var count = GetFulfilledCount(have, required);
+ return count == required.Count();
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
new file mode 100644
index 0000000000..b869b2b179
--- /dev/null
+++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
@@ -0,0 +1,5 @@
+count-requirement-constant-reason = exactly {$count}
+count-requirement-range-minimum-reason = at least {$minimum}
+count-requirement-range-maximum-reason = at most {$maximum}
+count-requirement-range-minmax-reason = between {$minimum} and {$maximum}
+count-requirement-all-reason = all
From 2ff98abfd4767c0a3cb06350520970d2325a44b5 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:05:01 -0500
Subject: [PATCH 08/54] Awagga
---
.../Locale/en-US/_DEN/requirements/playtime-requirements.ftl | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
index 70f4ad6099..1b7a38a63b 100644
--- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -2,9 +2,9 @@ player-requirement-playtime-time = {$playtime} minutes
player-requirement-playtime-minimum-time = at least {$playtime}
player-requirement-playtime-maximum-time = less than {$playtime}
player-requirement-playtime-minmax-time = between {$minimum} and {$maximum}
-player-requirement-playtime-constraint-reason = Must {$inverted ->
+player-requirement-playtime-constraint-reason = Must{$inverted ->
[true] not
- *[false] {""}
+ *[false] {" "}
} have {$timeConstraint}
player-requirement-department-playtime-reason = {$constraint} in the {$department} department.
From b7afe20a9d962aa131c9e5975b6c9b81d5dadb83 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:05:51 -0500
Subject: [PATCH 09/54] add trait requirements
---
.../PlayerRequirements.Profile.cs | 67 +++++++++++++++++++
.../requirements/profile-requirements.ftl | 5 ++
2 files changed, 72 insertions(+)
create mode 100644 Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
create mode 100644 Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
new file mode 100644
index 0000000000..129464701b
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -0,0 +1,67 @@
+using System.Linq;
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared._DEN.Traits.Prototypes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._DEN.Requirements.PlayerRequirements;
+
+///
+/// Checks if a player's character has a required number of the given traits.
+///
+public sealed partial class PlayerTraitRequirement : PlayerRequirement
+{
+ ///
+ /// Traits that the character needs to have to the pass the requirement.
+ ///
+ [DataField]
+ public HashSet> Traits = new();
+
+ [DataField]
+ public CountRequirement Count;
+
+ ///
+ public override bool PreCheck(PlayerRequirementContext context)
+ {
+ return context.Profile != null;
+ }
+
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ if (context.Profile == null)
+ return false;
+
+ var profileTraits = context.Profile.EntityTraitPreferences;
+ return Count.CheckRequirement(profileTraits, Traits);
+ }
+
+ ///
+ public override string? GetReason(PlayerRequirementContext context)
+ {
+ var protoMan = IoCManager.Resolve();
+ var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan));
+ var traitList = string.Join(", ", traitNames);
+ var constraintReason = Count.GetReason();
+
+ return Loc.GetString("player-requirement-trait-reason",
+ ("inverted", Inverted),
+ ("constraint", constraintReason),
+ ("traits", traitList));
+ }
+
+ ///
+ /// Localizes a trait ID into a formatted trait name.
+ ///
+ /// The ID of the trait.
+ /// The prototype manager.
+ /// A formatted trait name string.
+ private static string LocalizeTrait(ProtoId traitId, IPrototypeManager protoMan)
+ {
+ var traitName = traitId;
+
+ if (protoMan.TryIndex(traitId, out var trait))
+ traitName = Loc.GetString(trait.Name);
+
+ return Loc.GetString("player-requirement-trait", ("traitName", traitName));
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
new file mode 100644
index 0000000000..79abfd63bb
--- /dev/null
+++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
@@ -0,0 +1,5 @@
+player-requirement-trait = [color=LightBlue]{$traitName}[/color]
+player-requirement-trait-reason = Must{$inverted ->
+ [true] not
+ *[false] {" "}
+} have {$constraint} of these traits: {$traits};
From 207c653e7c7973fb987546a29894bc1e9a56260d Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:15:30 -0500
Subject: [PATCH 10/54] add player requirements to IOC
---
Content.Shared/IoC/SharedContentIoC.cs | 4 +++-
.../_DEN/Requirements/Managers/PlayerRequirementManager.cs | 6 ++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs
index f23b9f8355..fd69196de0 100644
--- a/Content.Shared/IoC/SharedContentIoC.cs
+++ b/Content.Shared/IoC/SharedContentIoC.cs
@@ -1,4 +1,5 @@
-using Content.Shared.Humanoid.Markings;
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared.Humanoid.Markings;
using Content.Shared.Localizations;
namespace Content.Shared.IoC
@@ -9,6 +10,7 @@ public static void Register(IDependencyCollection deps)
{
deps.Register();
deps.Register();
+ deps.Register(); // DEN
}
}
}
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
index 8eafb3d728..696b63ee07 100644
--- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -1,8 +1,14 @@
using Content.Shared._DEN.Requirements.PlayerRequirements;
using JetBrains.Annotations;
+#pragma warning disable IDE1006 // Naming Styles
namespace Content.Shared._DEN.Requirements.Managers;
+#pragma warning restore IDE1006 // Naming Styles
+///
+/// A manager used to check player stats against a list of requirements, getting the pass/fail status of these requirements.
+/// This can be used to apply restrictions to character actions, like jobs or traits.
+///
public sealed partial class PlayerRequirementManager
{
///
From fdd11f242f036eb09346c2c5e71bf80d42535655 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:48:58 -0500
Subject: [PATCH 11/54] comments
---
.../_DEN/Requirements/PlayerRequirements/CountRequirement.cs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
index 9a3b3df26a..a22a7d40e9 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -60,6 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement
///
public override string GetReason()
{
+ // "You must have exactly 1 of the following items."
return Loc.GetString("count-requirement-constant-reason",
("count", Count));
}
@@ -94,13 +95,16 @@ public override string GetReason()
{
return (Min, Max) switch
{
+ // "You must have between 1 and 5 of the following items."
(not null, not null) => Loc.GetString("count-requirement-range-minmax-reason",
("minimum", Min),
("maximum", Max)),
+ // "You must have at most 5 of the following items."
(null, not null) => Loc.GetString("count-requirement-range-maximum-reason",
("maximum", Max)),
+ // "You must have at least 1 of the following items."
(not null, null) => Loc.GetString("count-requirement-range-minimum-reason",
("minimum", Min)),
@@ -125,6 +129,7 @@ public sealed partial class AllCountRequirement : CountRequirement
///
public override string GetReason()
{
+ // "You must have all of the following items."
return Loc.GetString("count-requirement-all-reason");
}
From 6942b61c5f73b84719187877c40e8537b6f0aabc Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:05:17 -0500
Subject: [PATCH 12/54] add auto-pass to playtime requirements
---
.../PlayerRequirements.Playtime.cs | 33 ++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index b51b7cb060..b22bc123e3 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -1,5 +1,7 @@
using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared.CCVar;
using Content.Shared.Roles;
+using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Shared._DEN.Requirements.PlayerRequirements;
@@ -23,7 +25,10 @@ public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement
///
public override bool PreCheck(PlayerRequirementContext context)
{
- return context.Playtimes != null;
+ // We are always returning "true" if ShouldAutoPass() is true, because otherwise, if the
+ // pre-check failed, then it would be possible to fail this requirement as per
+ // PlayerRequirement.MustPassPreCheck even when role timers should be ignored anyway.
+ return ShouldAutoPass() || context.Playtimes != null;
}
///
@@ -42,6 +47,20 @@ protected bool IsValidPlaytime(TimeSpan playtime)
return true;
}
+ ///
+ /// Whether or not this requirement should auto-pass. This applies if role timers
+ /// are disabled, because playtimes shouldn't matter anyway in this case - we shouldn't
+ /// fail playtime requirements ever when role timers are disabled.
+ ///
+ ///
+ /// Whether or not this requirement should auto-pass.
+ ///
+ protected static bool ShouldAutoPass()
+ {
+ var config = IoCManager.Resolve();
+ return !config.GetCVar(CCVars.GameRoleTimers);
+ }
+
///
/// Gets a localized "reason" string for this requirement's playtime ranges.
///
@@ -53,6 +72,10 @@ protected bool IsValidPlaytime(TimeSpan playtime)
///
protected string? GetPlaytimeConstraintReason()
{
+ var config = IoCManager.Resolve();
+ if (!config.GetCVar(CCVars.GameRoleTimers))
+ return null;
+
if (MinTime == null && MaxTime == null)
return null;
@@ -103,6 +126,10 @@ public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytime
///
public override bool CheckRequirement(PlayerRequirementContext context)
{
+ // Auto-pass if role timers are disabled.
+ if (ShouldAutoPass())
+ return true;
+
var playtime = GetDepartmentPlaytime(context);
if (playtime is null)
return false;
@@ -113,6 +140,10 @@ public override bool CheckRequirement(PlayerRequirementContext context)
///
public override string? GetReason(PlayerRequirementContext context)
{
+ // Do not give a reason if role timers are disabled.
+ if (ShouldAutoPass())
+ return null;
+
// Get the department name.
var protoMan = IoCManager.Resolve();
if (!protoMan.TryIndex(Department, out var department))
From 5cd22c64b31a7e83c14e8eea23178dce8c4e302e Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:09:09 -0500
Subject: [PATCH 13/54] add IPlayerRequirementManager interface
---
.../Managers/IPlayerRequirementManager.cs | 22 +++++++++++++++++++
...Context.cs => PlayerRequirementContext.cs} | 0
...r.cs => SharedPlayerRequirementManager.cs} | 17 ++++++--------
3 files changed, 29 insertions(+), 10 deletions(-)
create mode 100644 Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
rename Content.Shared/_DEN/Requirements/Managers/{PlayerRequirementManager.Context.cs => PlayerRequirementContext.cs} (100%)
rename Content.Shared/_DEN/Requirements/Managers/{PlayerRequirementManager.cs => SharedPlayerRequirementManager.cs} (61%)
diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
new file mode 100644
index 0000000000..03dd15e39d
--- /dev/null
+++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
@@ -0,0 +1,22 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
+using Robust.Shared.Player;
+
+namespace Content.Shared._DEN.Requirements.Managers;
+
+public interface IPlayerRequirementManager
+{
+ ///
+ /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements.
+ ///
+ /// The context containing fields to check against the requirements.
+ /// An enumerable collection of requirements.
+ /// Whether or not this context passes *all* requirements. If even one fails, then this is false.
+ bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements);
+
+ ///
+ /// Creates a new PlayerRequirementContext with context fields pre-filled.
+ ///
+ /// The session associated with this player.
+ /// A pre-filled requirement context for this player.
+ PlayerRequirementContext GetPlayerContext(ICommonSession session);
+}
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs
similarity index 100%
rename from Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.Context.cs
rename to Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
similarity index 61%
rename from Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
rename to Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
index 696b63ee07..4a317b521c 100644
--- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
@@ -1,24 +1,18 @@
using Content.Shared._DEN.Requirements.PlayerRequirements;
using JetBrains.Annotations;
+using Robust.Shared.Player;
-#pragma warning disable IDE1006 // Naming Styles
namespace Content.Shared._DEN.Requirements.Managers;
-#pragma warning restore IDE1006 // Naming Styles
///
/// A manager used to check player stats against a list of requirements, getting the pass/fail status of these requirements.
/// This can be used to apply restrictions to character actions, like jobs or traits.
///
-public sealed partial class PlayerRequirementManager
+public abstract partial class SharedPlayerRequirementManager : IPlayerRequirementManager
{
- ///
- /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements.
- ///
- /// The context containing fields to check against the requirements.
- /// An enumerable collection of requirements.
- /// Whether or not this context passes *all* requirements. If even one fails, then this is false.
+ ///
[PublicAPI]
- public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements)
+ public bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements)
{
foreach (var requirement in requirements)
{
@@ -40,4 +34,7 @@ public static bool CheckRequirements(PlayerRequirementContext context, IEnumerab
return true;
}
+
+ ///
+ public abstract PlayerRequirementContext GetPlayerContext(ICommonSession session);
}
From d4ac7533bb744eff3de09d92587899801d3b712f Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 19:48:15 -0500
Subject: [PATCH 14/54] add "any" count requirement
---
.../PlayerRequirements/CountRequirement.cs | 36 +++++++++++++++----
.../_DEN/requirements/requirement-range.ftl | 1 +
2 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
index a22a7d40e9..96313bc487 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -17,7 +17,7 @@ public abstract partial class CountRequirement
/// Gets a string reason representation for this range.
///
///
- /// This would slot into the sentence "You must have [reason] of the following items: [items]".
+ /// This would slot into the sentence "Must have [reason] of the following items: [items]".
///
/// "At least 1", "between 2 and 5", "all"
/// A string representation of this range's requirement bounds.
@@ -60,7 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement
///
public override string GetReason()
{
- // "You must have exactly 1 of the following items."
+ // "Must have exactly 1 of the following items."
return Loc.GetString("count-requirement-constant-reason",
("count", Count));
}
@@ -95,16 +95,16 @@ public override string GetReason()
{
return (Min, Max) switch
{
- // "You must have between 1 and 5 of the following items."
+ // "Must have between 1 and 5 of the following items."
(not null, not null) => Loc.GetString("count-requirement-range-minmax-reason",
("minimum", Min),
("maximum", Max)),
- // "You must have at most 5 of the following items."
+ // "Must have at most 5 of the following items."
(null, not null) => Loc.GetString("count-requirement-range-maximum-reason",
("maximum", Max)),
- // "You must have at least 1 of the following items."
+ // "Must have at least 1 of the following items."
(not null, null) => Loc.GetString("count-requirement-range-minimum-reason",
("minimum", Min)),
@@ -121,6 +121,30 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
}
}
+///
+/// To fulfill the requirement, you must have at least one item in the required collection.
+///
+///
+/// This is similar to , with a Min of 1, but it says "any" in the reason instead.
+/// This sounds smoother for inverted requirements; i.e. "Must not have any of the following items."
+///
+public sealed partial class AllCountRequirement : CountRequirement
+{
+ ///
+ public override string GetReason()
+ {
+ // "Must have any of the following items."
+ return Loc.GetString("count-requirement-any-reason");
+ }
+
+ ///
+ public override bool CheckRequirement(IEnumerable have, IEnumerable required)
+ {
+ var count = GetFulfilledCount(have, required);
+ return count >= 1;
+ }
+}
+
///
/// To fulfill the requirement, you must have all items in the required collection.
///
@@ -129,7 +153,7 @@ public sealed partial class AllCountRequirement : CountRequirement
///
public override string GetReason()
{
- // "You must have all of the following items."
+ // "Must have all of the following items."
return Loc.GetString("count-requirement-all-reason");
}
diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
index b869b2b179..63b2c095c6 100644
--- a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
@@ -2,4 +2,5 @@ count-requirement-constant-reason = exactly {$count}
count-requirement-range-minimum-reason = at least {$minimum}
count-requirement-range-maximum-reason = at most {$maximum}
count-requirement-range-minmax-reason = between {$minimum} and {$maximum}
+count-requirement-any-reason = any
count-requirement-all-reason = all
From fc85ec9615bc5a3bda590b7c731c579f05ac718e Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 19:48:32 -0500
Subject: [PATCH 15/54] misnamed it oops
---
.../_DEN/Requirements/PlayerRequirements/CountRequirement.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
index 96313bc487..9c0c152b21 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -128,7 +128,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
/// This is similar to , with a Min of 1, but it says "any" in the reason instead.
/// This sounds smoother for inverted requirements; i.e. "Must not have any of the following items."
///
-public sealed partial class AllCountRequirement : CountRequirement
+public sealed partial class AnyCountRequirement : CountRequirement
{
///
public override string GetReason()
From 06961110340c6052f83a88336e2819ec043e2eea Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Tue, 24 Mar 2026 21:23:51 -0500
Subject: [PATCH 16/54] add GetPlayerContext()
---
Content.Client/IoC/ClientContentIoC.cs | 3 ++
.../Managers/PlayerRequirementManager.cs | 31 +++++++++++++++++++
Content.Server/IoC/ServerContentIoC.cs | 3 ++
.../Managers/PlayerRequirementManager.cs | 26 ++++++++++++++++
Content.Shared/IoC/SharedContentIoC.cs | 4 +--
.../Managers/PlayerRequirementContext.cs | 2 +-
6 files changed, 65 insertions(+), 4 deletions(-)
create mode 100644 Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs
create mode 100644 Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index efaf88b052..2ebde9222c 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -28,6 +28,8 @@
using Content.Shared.IoC;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Players.RateLimiting;
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Client._DEN.Requirements.Managers;
namespace Content.Client.IoC
{
@@ -66,6 +68,7 @@ public static void Register(IDependencyCollection collection)
collection.Register();
collection.Register();
collection.Register();
+ collection.Register(); // DEN
}
}
}
diff --git a/Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs
new file mode 100644
index 0000000000..c2705ef754
--- /dev/null
+++ b/Content.Client/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -0,0 +1,31 @@
+using Content.Client.Lobby;
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared.Players.PlayTimeTracking;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+
+namespace Content.Client._DEN.Requirements.Managers;
+
+///
+public sealed partial class PlayerRequirementManager : SharedPlayerRequirementManager
+{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IClientPreferencesManager _preferences = default!;
+ [Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
+
+ ///
+ public override PlayerRequirementContext GetPlayerContext(ICommonSession session)
+ {
+ if (_player.LocalSession != session)
+ return new();
+
+ var playtimes = _playtimeManager.GetPlayTimes(session);
+ var profile = _preferences.Preferences?.SelectedCharacter;
+
+ return new()
+ {
+ Playtimes = playtimes,
+ Profile = profile,
+ };
+ }
+}
diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs
index 1c6d940e20..c57ca41539 100644
--- a/Content.Server/IoC/ServerContentIoC.cs
+++ b/Content.Server/IoC/ServerContentIoC.cs
@@ -1,3 +1,4 @@
+using Content.Server._DEN.Requirements.Managers;
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
@@ -24,6 +25,7 @@
using Content.Server.ServerUpdates;
using Content.Server.Voting.Managers;
using Content.Server.Worldgen.Tools;
+using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
@@ -84,5 +86,6 @@ public static void Register(IDependencyCollection deps)
deps.Register();
deps.Register();
deps.Register();
+ deps.Register(); // DEN
}
}
diff --git a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
new file mode 100644
index 0000000000..0732f4a7a1
--- /dev/null
+++ b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -0,0 +1,26 @@
+using Content.Server.GameTicking;
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared.Players.PlayTimeTracking;
+using Robust.Shared.Player;
+
+namespace Content.Server._DEN.Requirements.Managers;
+
+///
+public sealed partial class PlayerRequirementManager : SharedPlayerRequirementManager
+{
+ [Dependency] private readonly GameTicker _gameTicker = default!;
+ [Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
+
+ ///
+ public override PlayerRequirementContext GetPlayerContext(ICommonSession session)
+ {
+ var playtimes = _playtimeManager.GetPlayTimes(session);
+ var profile = _gameTicker.GetPlayerProfile(session);
+
+ return new()
+ {
+ Playtimes = playtimes,
+ Profile = profile,
+ };
+ }
+}
diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs
index fd69196de0..f23b9f8355 100644
--- a/Content.Shared/IoC/SharedContentIoC.cs
+++ b/Content.Shared/IoC/SharedContentIoC.cs
@@ -1,5 +1,4 @@
-using Content.Shared._DEN.Requirements.Managers;
-using Content.Shared.Humanoid.Markings;
+using Content.Shared.Humanoid.Markings;
using Content.Shared.Localizations;
namespace Content.Shared.IoC
@@ -10,7 +9,6 @@ public static void Register(IDependencyCollection deps)
{
deps.Register();
deps.Register();
- deps.Register(); // DEN
}
}
}
diff --git a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs
index 1737ea121f..d2da99b62a 100644
--- a/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/PlayerRequirementContext.cs
@@ -18,5 +18,5 @@ public partial record PlayerRequirementContext
///
/// A dictionary of registered playtimes for the player.
///
- public Dictionary, TimeSpan>? Playtimes = null;
+ public IReadOnlyDictionary? Playtimes = null;
}
From ffbdbd956f17691fc85c0a53db452e0c0093d001 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 02:53:22 -0500
Subject: [PATCH 17/54] get tooltips and trait requirements working
---
.../UI/Traits/EntityTraitSelector.xaml.cs | 59 ++++++++++++++++++-
.../Managers/PlayerRequirementManager.cs | 15 ++++-
.../_DEN/Traits/EntitySystems/TraitSystem.cs | 14 ++++-
.../Preferences/HumanoidCharacterProfile.cs | 14 ++++-
.../Managers/IPlayerRequirementManager.cs | 4 ++
.../PlayerRequirements/IPlayerRequirement.cs | 8 +--
.../PlayerRequirements.Playtime.cs | 35 +++++------
.../PlayerRequirements.Profile.cs | 4 +-
.../Traits/Prototypes/EntityTraitPrototype.cs | 7 +++
.../_DEN/requirements/formatted-fields.ftl | 3 +
.../requirements/playtime-requirements.ftl | 5 +-
.../requirements/profile-requirements.ftl | 5 +-
12 files changed, 131 insertions(+), 42 deletions(-)
create mode 100644 Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
index 12cc197c9e..971af64d3e 100644
--- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
+++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
@@ -1,8 +1,12 @@
+using System.Text;
using Content.Shared._DEN.Traits.Prototypes;
using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
namespace Content.Client._DEN.Lobby.UI.Traits;
@@ -19,6 +23,7 @@ public bool Preference
}
public ProtoId? PrototypeId { private set; get; } = null;
+ private EntityTraitPrototype? _trait = null;
public EntityTraitSelector(EntityTraitPrototype trait)
{
@@ -26,12 +31,14 @@ public EntityTraitSelector(EntityTraitPrototype trait)
SetTrait(trait);
SelectorCheckbox.OnToggled += args => { PreferenceChanged?.Invoke(args.Pressed); };
+ SelectorCheckbox.TooltipSupplier = SupplyTooltip;
}
private void SetTrait(EntityTraitPrototype trait)
{
Cost = trait.Cost;
PrototypeId = trait.ID;
+ _trait = trait;
var text = "";
@@ -40,9 +47,59 @@ private void SetTrait(EntityTraitPrototype trait)
text += Loc.GetString(trait.Name);
SelectorCheckbox.Text = text;
+ }
+
+ ///
+ /// Provide a tooltip to this control if it has a valid trait set.
+ /// This includes the trait's description and its requirement text.
+ ///
+ /// A tooltip with the trait's description, if this control has a trait.
+ private Tooltip? SupplyTooltip(Control sender)
+ {
+ if (_trait is null)
+ return null;
+
+ var tooltip = new Tooltip();
+ var tooltipString = ConstructTooltipDescription(_trait);
+
+ if (FormattedMessage.TryFromMarkup(tooltipString, out var msg))
+ tooltip.SetMessage(msg);
+
+ return tooltip;
+ }
+
+ ///
+ /// Construct a full tooltip description for a given trait.
+ ///
+ /// The trait to construct a description out of.
+ /// The tooltip text associated with this trait.
+ private static string ConstructTooltipDescription(EntityTraitPrototype trait)
+ {
+ var tooltipBuilder = new StringBuilder();
if (trait.Description is not null)
- SelectorCheckbox.ToolTip = Loc.GetString(trait.Description.Value);
+ tooltipBuilder.AppendLine(Loc.GetString(trait.Description.Value));
+
+ if (trait.Requirements.Count > 0)
+ {
+ tooltipBuilder.AppendLine(); // Empty line
+ foreach (var requirement in trait.Requirements)
+ {
+ var reason = requirement.GetReason();
+ if (reason is null)
+ continue;
+
+ tooltipBuilder.AppendLine(reason);
+ }
+ }
+
+ var tooltipString = tooltipBuilder.ToString();
+ var lineBreak = "\n";
+
+ if (tooltipString.EndsWith(lineBreak))
+ tooltipString = tooltipString[..^lineBreak.Length];
+
+ return tooltipString;
}
public void SetInvalid(bool invalid)
diff --git a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
index 0732f4a7a1..63d27ebe68 100644
--- a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
+++ b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -1,6 +1,7 @@
-using Content.Server.GameTicking;
+using Content.Server.Preferences.Managers;
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.Players.PlayTimeTracking;
+using Content.Shared.Preferences;
using Robust.Shared.Player;
namespace Content.Server._DEN.Requirements.Managers;
@@ -8,14 +9,14 @@ namespace Content.Server._DEN.Requirements.Managers;
///
public sealed partial class PlayerRequirementManager : SharedPlayerRequirementManager
{
- [Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
+ [Dependency] private readonly IServerPreferencesManager _prefsManager = default!;
///
public override PlayerRequirementContext GetPlayerContext(ICommonSession session)
{
var playtimes = _playtimeManager.GetPlayTimes(session);
- var profile = _gameTicker.GetPlayerProfile(session);
+ var profile = GetProfile(session);
return new()
{
@@ -23,4 +24,12 @@ public override PlayerRequirementContext GetPlayerContext(ICommonSession session
Profile = profile,
};
}
+
+ private HumanoidCharacterProfile? GetProfile(ICommonSession session)
+ {
+ if (!_prefsManager.TryGetCachedPreferences(session.UserId, out var prefs))
+ return null;
+
+ return prefs.SelectedCharacter;
+ }
}
diff --git a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
index 997672410d..9c3088e1a0 100644
--- a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
+++ b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
@@ -1,10 +1,8 @@
+using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Traits.EntitySystems;
-using Content.Shared._DEN.Traits.Prototypes;
using Content.Shared.GameTicking;
-using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Roles;
-using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
#pragma warning disable IDE1006 // Naming Styles
@@ -14,6 +12,7 @@ namespace Content.Server._DEN.Traits.EntitySystems;
public sealed partial class TraitSystem : SharedTraitSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlayerRequirementManager _requirements = default!;
public override void Initialize()
{
@@ -45,6 +44,7 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
continue;
}
+ // TODO DEN: Remove
if (trait.AllowedSpecies != null)
{
if (!TryComp(mob, out var profile)
@@ -55,6 +55,14 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
}
}
+ var context = _requirements.GetPlayerContext(args.Player);
+ context.Profile = args.Profile;
+ if (!_requirements.CheckRequirements(context, trait.Requirements))
+ {
+ Log.Error($"Tried to spawn trait {traitId} on {ToPrettyString(mob)}, but we failed the requirements to do so!");
+ continue;
+ }
+
TryAddTrait(mob, traitId, out _);
}
}
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index e117771313..4c26451021 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -21,6 +21,7 @@
using Robust.Shared;
using YamlDotNet.RepresentationModel;
using Content.Shared._DEN.Traits.Prototypes;
+using Content.Shared._DEN.Requirements.Managers;
namespace Content.Shared.Preferences
{
@@ -489,6 +490,7 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
{
var configManager = collection.Resolve();
var prototypeManager = collection.Resolve();
+ var requirements = collection.Resolve(); // DEN
if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
{
@@ -605,15 +607,21 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
.Where(id => prototypeManager.TryIndex(id, out var antag) && antag.SetPreference)
.ToList();
+ // Begin DEN: Trait validation
// var traits = TraitPreferences
// .Where(prototypeManager.HasIndex)
- // .ToList(); // DEN
+ // .ToList();
+
+ var context = requirements.GetPlayerContext(session);
+ context.Profile = this;
var traits = EntityTraitPreferences
.Where(t => prototypeManager.TryIndex(t, out var trait)
&& trait.Selectable
- && (trait.AllowedSpecies is null || trait.AllowedSpecies.Contains(Species)))
- .ToList(); // DEN
+ && (trait.AllowedSpecies is null || trait.AllowedSpecies.Contains(Species)) // TODO DEN: Remove
+ && requirements.CheckRequirements(context, trait.Requirements))
+ .ToList();
+ // End DEN
Name = name;
FlavorText = flavortext;
diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
index 03dd15e39d..234ade55fd 100644
--- a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
@@ -3,6 +3,10 @@
namespace Content.Shared._DEN.Requirements.Managers;
+///
+/// A manager used to check player stats against a list of requirements, getting the pass/fail status of these requirements.
+/// This can be used to apply restrictions to character actions, like jobs or traits.
+///
public interface IPlayerRequirementManager
{
///
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 2458beefec..32edd51361 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -58,12 +58,10 @@ public partial interface IPlayerRequirement
/// Get a localized string representing how this requirement displays to players.
///
///
- /// This should, ideally, display regardless if the context actually passes or not.
- /// It should also display differently depending on the value of .
+ /// This should display differently depending on the value of .
///
- /// A definition of parameters to check against the requirement.
/// An optional string representing the requirement text to display to a player.
- string? GetReason(PlayerRequirementContext context);
+ string? GetReason();
}
///
@@ -81,7 +79,7 @@ public abstract partial class PlayerRequirement : IPlayerRequirement
public abstract bool CheckRequirement(PlayerRequirementContext context);
///
- public abstract string? GetReason(PlayerRequirementContext context);
+ public abstract string? GetReason();
///
public abstract bool PreCheck(PlayerRequirementContext context);
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index b22bc123e3..941dc089f4 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -1,5 +1,6 @@
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.CCVar;
+using Content.Shared.Localizations;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
@@ -72,17 +73,13 @@ protected static bool ShouldAutoPass()
///
protected string? GetPlaytimeConstraintReason()
{
- var config = IoCManager.Resolve();
- if (!config.GetCVar(CCVars.GameRoleTimers))
- return null;
-
if (MinTime == null && MaxTime == null)
return null;
var minTimeString = FormatPlaytime(MinTime);
var maxTimeString = FormatPlaytime(MaxTime);
- return (MinTime, MaxTime) switch
+ return (minTimeString, maxTimeString) switch
{
(not null, not null) => Loc.GetString("player-requirement-playtime-minmax-time",
("minimum", minTimeString), ("maximum", maxTimeString)),
@@ -97,18 +94,14 @@ protected static bool ShouldAutoPass()
};
}
- ///
- /// Gets a localized time string for the given playtime.
- ///
- /// The playtime to format.
- /// A localized time string for this playtime.
- private static string FormatPlaytime(TimeSpan? playtime)
+ private static string? FormatPlaytime(TimeSpan? playtime)
{
- var time = ((int?)playtime?.TotalMinutes) ?? 0;
+ if (playtime is null)
+ return null;
- return playtime != null
- ? Loc.GetString("player-requirement-playtime-time", ("playtime", time))
- : string.Empty;
+ var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value);
+ return Loc.GetString("player-requirement-format-time",
+ ("playtime", playtimeString));
}
}
@@ -138,18 +131,22 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason(PlayerRequirementContext context)
+ public override string? GetReason()
{
// Do not give a reason if role timers are disabled.
if (ShouldAutoPass())
return null;
- // Get the department name.
+ // Get the department name and format it with a color.
var protoMan = IoCManager.Resolve();
if (!protoMan.TryIndex(Department, out var department))
return null;
var deptName = Loc.GetString(department.Name);
+ var deptColor = department.Color.ToHex();
+ var formattedDept = Loc.GetString("player-requirement-format-department",
+ ("color", deptColor),
+ ("department", deptName));
// Get the playtime constraint string.
var playtimeString = GetPlaytimeConstraintReason();
@@ -160,10 +157,10 @@ public override bool CheckRequirement(PlayerRequirementContext context)
("inverted", Inverted),
("timeConstraint", playtimeString));
- // E.g. "You must have 120 minutes in the Science department."
+ // E.g. "You must have 2h30m in the Science department."
return Loc.GetString("player-requirement-department-playtime-reason",
("constraint", constraintReason),
- ("department", deptName));
+ ("department", formattedDept));
}
///
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index 129464701b..df482d9390 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -36,7 +36,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason(PlayerRequirementContext context)
+ public override string? GetReason()
{
var protoMan = IoCManager.Resolve();
var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan));
@@ -62,6 +62,6 @@ private static string LocalizeTrait(ProtoId traitId, IProt
if (protoMan.TryIndex(traitId, out var trait))
traitName = Loc.GetString(trait.Name);
- return Loc.GetString("player-requirement-trait", ("traitName", traitName));
+ return Loc.GetString("player-requirement-format-trait", ("trait", traitName));
}
}
diff --git a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs
index 70e6fc7dde..40cc398f54 100644
--- a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs
+++ b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs
@@ -1,3 +1,4 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
using Content.Shared._DEN.Traits.TraitFunctions;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Traits;
@@ -69,4 +70,10 @@ public sealed partial class EntityTraitPrototype : IPrototype
///
[DataField("characterEditorSelectable")]
public bool Selectable = true;
+
+ ///
+ /// A list of requirements to use this trait.
+ ///
+ [DataField]
+ public List Requirements = new();
}
diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
new file mode 100644
index 0000000000..891ed33750
--- /dev/null
+++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
@@ -0,0 +1,3 @@
+player-requirement-format-department = [color={$color}]{$department}[/color]
+player-requirement-format-time = [color=yellow]{$playtime}[/color]
+player-requirement-format-trait = [color=LightBlue]{$trait}[/color]
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
index 1b7a38a63b..9775b9a01d 100644
--- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -1,10 +1,9 @@
-player-requirement-playtime-time = {$playtime} minutes
player-requirement-playtime-minimum-time = at least {$playtime}
player-requirement-playtime-maximum-time = less than {$playtime}
player-requirement-playtime-minmax-time = between {$minimum} and {$maximum}
player-requirement-playtime-constraint-reason = Must{$inverted ->
- [true] not
- *[false] {" "}
+ [true] {" "}not
+ *[false] {""}
} have {$timeConstraint}
player-requirement-department-playtime-reason = {$constraint} in the {$department} department.
diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
index 79abfd63bb..970eb2759b 100644
--- a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
@@ -1,5 +1,4 @@
-player-requirement-trait = [color=LightBlue]{$traitName}[/color]
player-requirement-trait-reason = Must{$inverted ->
- [true] not
+ [true] {" "}not
*[false] {" "}
-} have {$constraint} of these traits: {$traits};
+} have {$constraint} of these traits: {$traits}
From 0405af2dcac259fa00b0a031531ad8141bf39a10 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:16:41 -0500
Subject: [PATCH 18/54] clean up tooltip code
---
.../Lobby/UI/Traits/EntityTraitSelector.xaml.cs | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
index 971af64d3e..b590fa0cff 100644
--- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
+++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
@@ -59,9 +59,11 @@ private void SetTrait(EntityTraitPrototype trait)
if (_trait is null)
return null;
- var tooltip = new Tooltip();
var tooltipString = ConstructTooltipDescription(_trait);
+ if (tooltipString.Length == 0)
+ return null;
+ var tooltip = new Tooltip();
if (FormattedMessage.TryFromMarkup(tooltipString, out var msg))
tooltip.SetMessage(msg);
@@ -77,9 +79,11 @@ private static string ConstructTooltipDescription(EntityTraitPrototype trait)
{
var tooltipBuilder = new StringBuilder();
+ // Add description
if (trait.Description is not null)
tooltipBuilder.AppendLine(Loc.GetString(trait.Description.Value));
+ // Add requirement reason texts
if (trait.Requirements.Count > 0)
{
tooltipBuilder.AppendLine(); // Empty line
@@ -93,12 +97,7 @@ private static string ConstructTooltipDescription(EntityTraitPrototype trait)
}
}
- var tooltipString = tooltipBuilder.ToString();
- var lineBreak = "\n";
-
- if (tooltipString.EndsWith(lineBreak))
- tooltipString = tooltipString[..^lineBreak.Length];
-
+ var tooltipString = tooltipBuilder.ToString().Trim();
return tooltipString;
}
From b45bab7818cafb63bfadd208b162b43114b7dba8 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:16:47 -0500
Subject: [PATCH 19/54] add species requirements
---
.../PlayerRequirements.Profile.cs | 62 +++++++++++++++++++
.../_DEN/requirements/formatted-fields.ftl | 3 +-
.../requirements/profile-requirements.ftl | 7 ++-
3 files changed, 70 insertions(+), 2 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index df482d9390..2fbfd17e05 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -1,6 +1,7 @@
using System.Linq;
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Traits.Prototypes;
+using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._DEN.Requirements.PlayerRequirements;
@@ -65,3 +66,64 @@ private static string LocalizeTrait(ProtoId traitId, IProt
return Loc.GetString("player-requirement-format-trait", ("trait", traitName));
}
}
+
+///
+/// Checks if a player's character is one of the following species.
+///
+public sealed partial class PlayerSpeciesRequirement : PlayerRequirement
+{
+ ///
+ /// Character's profile must be one of these species to pass the requirement.
+ ///
+ [DataField]
+ public HashSet> Species = new();
+
+ ///
+ public override bool PreCheck(PlayerRequirementContext context)
+ {
+ return context.Profile != null;
+ }
+
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ if (context.Profile == null)
+ return false;
+
+ return Species.Contains(context.Profile.Species);
+ }
+
+ ///
+ public override string? GetReason()
+ {
+ var protoMan = IoCManager.Resolve();
+
+ // Add all species names to a list
+ var speciesList = new List();
+ foreach (var speciesId in Species)
+ speciesList.Add(LocalizeSpecies(speciesId, protoMan));
+
+ var joinedSpecies = string.Join(", ", speciesList);
+
+ // "Must be one of these species: Human, Dwarf, Slimeperson"
+ return Loc.GetString("player-requirement-species-reason",
+ ("inverted", Inverted),
+ ("species", joinedSpecies));
+ }
+
+ ///
+ /// Localizes a species ID into a formatted species name.
+ ///
+ /// The ID of the species.
+ /// The prototype manager.
+ /// A formatted species name string.
+ private static string LocalizeSpecies(ProtoId speciesId, IPrototypeManager protoMan)
+ {
+ var speciesName = speciesId;
+ if (!protoMan.TryIndex(speciesId, out var species))
+ return speciesName;
+
+ speciesName = Loc.GetString(species.Name);
+ return Loc.GetString("player-requirement-format-species", ("species", speciesName));
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
index 891ed33750..e5fdb2b215 100644
--- a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
@@ -1,3 +1,4 @@
player-requirement-format-department = [color={$color}]{$department}[/color]
-player-requirement-format-time = [color=yellow]{$playtime}[/color]
+player-requirement-format-species = [color=Green]{$species}[/color]
+player-requirement-format-time = [color=Yellow]{$playtime}[/color]
player-requirement-format-trait = [color=LightBlue]{$trait}[/color]
diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
index 970eb2759b..02bce0b096 100644
--- a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
@@ -1,4 +1,9 @@
player-requirement-trait-reason = Must{$inverted ->
[true] {" "}not
- *[false] {" "}
+ *[false] {""}
} have {$constraint} of these traits: {$traits}
+
+player-requirement-species-reason = Must{$inverted ->
+ [true] {" "}not
+ *[false] {""}
+} be one of these species: {$species}
From 9cf0f61b4735a67d369684ba801a0d4822f83f7a Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:21:16 -0500
Subject: [PATCH 20/54] add a missing comment
---
.../_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 32edd51361..8a0bc09d9d 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -44,7 +44,7 @@ public partial interface IPlayerRequirement
/// .
///
/// A definition of parameters to check against the requirement.
- ///
+ /// Whether or not the context has all required parameters.
bool PreCheck(PlayerRequirementContext context);
///
From 58122c5df45385ed81d879eb650604d353a6dd00 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:26:06 -0500
Subject: [PATCH 21/54] comment this method
---
.../_DEN/Requirements/Managers/PlayerRequirementManager.cs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
index 63d27ebe68..ea93973b6b 100644
--- a/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
+++ b/Content.Server/_DEN/Requirements/Managers/PlayerRequirementManager.cs
@@ -25,6 +25,11 @@ public override PlayerRequirementContext GetPlayerContext(ICommonSession session
};
}
+ ///
+ /// Retrieves a session's selected lobby character.
+ ///
+ /// The session to retrieve a profile for.
+ /// The session's currently-selected profile, if any.
private HumanoidCharacterProfile? GetProfile(ICommonSession session)
{
if (!_prefsManager.TryGetCachedPreferences(session.UserId, out var prefs))
From 3522c3233ef74e6cb02c3a2accc05d5a7dabdf9c Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:45:44 -0500
Subject: [PATCH 22/54] simplify this realized it doesnt need to be removed
from the list if it fails requirements because requirements are contextual
---
Content.Shared/Preferences/HumanoidCharacterProfile.cs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index 4c26451021..86252c1a53 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -616,10 +616,7 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
context.Profile = this;
var traits = EntityTraitPreferences
- .Where(t => prototypeManager.TryIndex(t, out var trait)
- && trait.Selectable
- && (trait.AllowedSpecies is null || trait.AllowedSpecies.Contains(Species)) // TODO DEN: Remove
- && requirements.CheckRequirements(context, trait.Requirements))
+ .Where(t => prototypeManager.TryIndex(t, out var trait) && trait.Selectable)
.ToList();
// End DEN
From 3650bc95cce46b1d9d1973e8bc304e8a110359b3 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 04:05:27 -0500
Subject: [PATCH 23/54] add hiding traits from UI
---
.../UI/Traits/EntityTraitSelector.xaml.cs | 21 ++++++++
.../Managers/IPlayerRequirementManager.cs | 9 ++++
.../SharedPlayerRequirementManager.cs | 49 +++++++++++++------
.../PlayerRequirements/IPlayerRequirement.cs | 9 ++++
4 files changed, 73 insertions(+), 15 deletions(-)
diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
index b590fa0cff..2d9c28351b 100644
--- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
+++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
@@ -1,6 +1,8 @@
using System.Text;
+using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Traits.Prototypes;
using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -13,6 +15,9 @@ namespace Content.Client._DEN.Lobby.UI.Traits;
[GenerateTypedNameReferences]
public sealed partial class EntityTraitSelector : BoxContainer
{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IPlayerRequirementManager _requirements = default!;
+
public event Action? PreferenceChanged;
public int Cost { private set; get; } = 0;
@@ -28,6 +33,7 @@ public bool Preference
public EntityTraitSelector(EntityTraitPrototype trait)
{
RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
SetTrait(trait);
SelectorCheckbox.OnToggled += args => { PreferenceChanged?.Invoke(args.Pressed); };
@@ -47,6 +53,21 @@ private void SetTrait(EntityTraitPrototype trait)
text += Loc.GetString(trait.Name);
SelectorCheckbox.Text = text;
+ UpdateVisibility(trait);
+ }
+
+ ///
+ /// Toggles the visibility of this selector, depending on the requirements of a given trait.
+ ///
+ /// The trait to check the requirements of.
+ private void UpdateVisibility(EntityTraitPrototype trait)
+ {
+ var session = _player.LocalSession;
+ if (session == null)
+ return;
+
+ var context = _requirements.GetPlayerContext(session);
+ Visible = !_requirements.ShouldHide(context, trait.Requirements);
}
///
diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
index 234ade55fd..297d3b03f6 100644
--- a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
@@ -17,6 +17,15 @@ public interface IPlayerRequirementManager
/// Whether or not this context passes *all* requirements. If even one fails, then this is false.
bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements);
+ ///
+ /// Whether or not the item associated with a set of requirements should be hidden
+ /// from the player, such as in UI.
+ ///
+ /// The context containing fields to check against the requirements.
+ /// An enumerable collection of requirements.
+ /// Whether or not the item should be hidden from the player.
+ bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements);
+
///
/// Creates a new PlayerRequirementContext with context fields pre-filled.
///
diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
index 4a317b521c..3205637ae7 100644
--- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
@@ -15,22 +15,41 @@ public abstract partial class SharedPlayerRequirementManager : IPlayerRequiremen
public bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements)
{
foreach (var requirement in requirements)
- {
- // Pre-check the requirement. This ensures our context has all the fields needed for the requirement.
- // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck.
- // If you don't need to pass the pre-check, then it's an auto-success.
- if (!requirement.PreCheck(context))
- {
- if (requirement.MustPassPreCheck)
- return false;
-
- continue;
- }
-
- // Check the actual requirement, now.
- if (!requirement.CheckRequirement(context))
+ if (!CheckRequirement(context, requirement))
return false;
- }
+
+ return true;
+ }
+
+ ///
+ [PublicAPI]
+ public bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements)
+ {
+ foreach (var requirement in requirements)
+ if (!CheckRequirement(context, requirement) && requirement.HideIfFailed)
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Check a single requirement for whether it passes/fails against a context.
+ ///
+ /// The context containing fields to check against the requirement.
+ /// The requirement to check.
+ /// Whether this context passes the requirement.
+ [PublicAPI]
+ public static bool CheckRequirement(PlayerRequirementContext context, IPlayerRequirement requirement)
+ {
+ // Pre-check the requirement. This ensures our context has all the fields needed for the requirement.
+ // If the pre-check fails, whether or not this requirement passes depends on requirement.MustPassPreCheck.
+ // If you don't need to pass the pre-check, then it's an auto-success.
+ if (!requirement.PreCheck(context))
+ return !requirement.MustPassPreCheck;
+
+ // Check the actual requirement, now.
+ if (!requirement.CheckRequirement(context))
+ return false;
return true;
}
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 8a0bc09d9d..5aeae911d5 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -35,6 +35,12 @@ public partial interface IPlayerRequirement
[DataField]
bool MustPassPreCheck { get; set; }
+ ///
+ /// Whether or not the item should be hidden from the UI if this requirement fails.
+ ///
+ [DataField]
+ bool HideIfFailed { get; set; }
+
///
/// Check if the given context has all parameters required in order to perform an actual check.
///
@@ -75,6 +81,9 @@ public abstract partial class PlayerRequirement : IPlayerRequirement
///
[DataField] public bool MustPassPreCheck { get; set; } = false;
+ ///
+ [DataField] public bool HideIfFailed { get; set; } = false;
+
///
public abstract bool CheckRequirement(PlayerRequirementContext context);
From b6f862e9f571d5dae18f63dfcf8c208e980320da Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 04:07:09 -0500
Subject: [PATCH 24/54] remove AllowedSpecies from EntityTraitPrototype
---
.../_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs | 3 +--
.../_DEN/Traits/EntitySystems/TraitSystem.cs | 11 -----------
.../_DEN/Traits/Prototypes/EntityTraitPrototype.cs | 7 -------
3 files changed, 1 insertion(+), 20 deletions(-)
diff --git a/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs
index 5a440bae60..32926df911 100644
--- a/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs
+++ b/Content.Client/_DEN/Lobby/UI/Traits/TraitCategoryBox.xaml.cs
@@ -46,8 +46,7 @@ public void SetTraits(List traits)
foreach (var trait in traits)
{
- if (!trait.Selectable || trait.AllowedSpecies is not null
- && (_profile?.Species is null || !trait.AllowedSpecies.Contains(_profile.Species)))
+ if (!trait.Selectable)
continue;
var selector = new EntityTraitSelector(trait);
diff --git a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
index 9c3088e1a0..ae503dc1d4 100644
--- a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
+++ b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
@@ -44,17 +44,6 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
continue;
}
- // TODO DEN: Remove
- if (trait.AllowedSpecies != null)
- {
- if (!TryComp(mob, out var profile)
- || !trait.AllowedSpecies.Contains(profile.Species))
- {
- Log.Error($"Tried to spawn trait {traitId} on {ToPrettyString(mob)} with invalid species: {profile?.Species ?? "null"}!");
- continue;
- }
- }
-
var context = _requirements.GetPlayerContext(args.Player);
context.Profile = args.Profile;
if (!_requirements.CheckRequirements(context, trait.Requirements))
diff --git a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs
index 40cc398f54..9553ca6a4e 100644
--- a/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs
+++ b/Content.Shared/_DEN/Traits/Prototypes/EntityTraitPrototype.cs
@@ -52,13 +52,6 @@ public sealed partial class EntityTraitPrototype : IPrototype
[DataField]
public ProtoId? Category;
- ///
- /// A list of species that allowed to take this trait. If null, then all species may take it.
- /// TODO: Replace with a more robust "requirement" system.
- ///
- [DataField]
- public HashSet>? AllowedSpecies = null;
-
///
/// A list of functions associated with this trait.
///
From 21349cc330a65d3d380e8cd1466f145c46833791 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 04:07:54 -0500
Subject: [PATCH 25/54] add species requirements to morphology traits
---
.../Prototypes/_DEN/EntityTraits/species.yml | 20 +++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/Resources/Prototypes/_DEN/EntityTraits/species.yml b/Resources/Prototypes/_DEN/EntityTraits/species.yml
index 71eae2c81b..d6a1610228 100644
--- a/Resources/Prototypes/_DEN/EntityTraits/species.yml
+++ b/Resources/Prototypes/_DEN/EntityTraits/species.yml
@@ -8,7 +8,10 @@
name: trait-morph-human-canid-name
description: trait-morph-human-canid-desc
category: SpeciesMorphs
- allowedSpecies: [Human]
+ requirements:
+ - !type:PlayerSpeciesRequirement
+ hideIfFailed: true
+ species: [Human]
cost: 1
whitelist:
components:
@@ -22,7 +25,10 @@
name: trait-morph-human-felinid-name
description: trait-morph-human-felinid-desc
category: SpeciesMorphs
- allowedSpecies: [Human]
+ requirements:
+ - !type:PlayerSpeciesRequirement
+ hideIfFailed: true
+ species: [Human]
cost: 1
whitelist:
components:
@@ -36,7 +42,10 @@
name: trait-morph-human-kitsune-name
description: trait-morph-human-kitsune-desc
category: SpeciesMorphs
- allowedSpecies: [Human]
+ requirements:
+ - !type:PlayerSpeciesRequirement
+ hideIfFailed: true
+ species: [Human]
cost: 1
whitelist:
components:
@@ -50,7 +59,10 @@
name: trait-morph-human-oni-name
description: trait-morph-human-oni-desc
category: SpeciesMorphs
- allowedSpecies: [Human]
+ requirements:
+ - !type:PlayerSpeciesRequirement
+ hideIfFailed: true
+ species: [Human]
cost: 1
whitelist:
components:
From fb11510174c03faccfed9e90cb4e553bc16cce28 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 11:54:36 -0500
Subject: [PATCH 26/54] obsolete this
---
Content.Shared/Roles/JobRequirements.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Content.Shared/Roles/JobRequirements.cs b/Content.Shared/Roles/JobRequirements.cs
index 62d50f8489..488d7a2cc4 100644
--- a/Content.Shared/Roles/JobRequirements.cs
+++ b/Content.Shared/Roles/JobRequirements.cs
@@ -6,6 +6,7 @@
namespace Content.Shared.Roles;
+[Obsolete("Use PlayerRequirements instead")] // DEN
public static class JobRequirements
{
///
@@ -62,6 +63,7 @@ public static bool TryRequirementsMet(
///
[ImplicitDataDefinitionForInheritors]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerRequirements instead")] // DEN
public abstract partial class JobRequirement
{
[DataField]
From 9ee17ee2ada5409c22deafd5ca9e4d5ffb7f04d8 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 13:25:36 -0500
Subject: [PATCH 27/54] add job playtime requirements
---
.../PlayerRequirements.Playtime.cs | 132 ++++++++++++++++--
.../_DEN/requirements/formatted-fields.ftl | 1 +
.../requirements/playtime-requirements.ftl | 3 +-
3 files changed, 124 insertions(+), 12 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 941dc089f4..9f8f928061 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -2,6 +2,7 @@
using Content.Shared.CCVar;
using Content.Shared.Localizations;
using Content.Shared.Roles;
+using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
@@ -137,16 +138,9 @@ public override bool CheckRequirement(PlayerRequirementContext context)
if (ShouldAutoPass())
return null;
- // Get the department name and format it with a color.
+ // Get a formatted department name.
var protoMan = IoCManager.Resolve();
- if (!protoMan.TryIndex(Department, out var department))
- return null;
-
- var deptName = Loc.GetString(department.Name);
- var deptColor = department.Color.ToHex();
- var formattedDept = Loc.GetString("player-requirement-format-department",
- ("color", deptColor),
- ("department", deptName));
+ var deptName = FormatDepartment(protoMan);
// Get the playtime constraint string.
var playtimeString = GetPlaytimeConstraintReason();
@@ -157,10 +151,29 @@ public override bool CheckRequirement(PlayerRequirementContext context)
("inverted", Inverted),
("timeConstraint", playtimeString));
- // E.g. "You must have 2h30m in the Science department."
+ // E.g. "You must have 2h30m of playtime in the Science department."
return Loc.GetString("player-requirement-department-playtime-reason",
("constraint", constraintReason),
- ("department", formattedDept));
+ ("department", deptName));
+ }
+
+ ///
+ /// Format this requirement's department name, with a color.
+ ///
+ /// The prototype manager.
+ /// The department name of this prototype, formatted.
+ private string FormatDepartment(IPrototypeManager protoMan)
+ {
+ if (!protoMan.TryIndex(Department, out var department))
+ return Department;
+
+ var deptName = Loc.GetString(department.Name);
+ var deptColor = department.Color.ToHex();
+ var formattedDept = Loc.GetString("player-requirement-format-department",
+ ("color", deptColor),
+ ("department", deptName));
+
+ return formattedDept;
}
///
@@ -192,3 +205,100 @@ public override bool CheckRequirement(PlayerRequirementContext context)
return playtime;
}
}
+
+///
+/// Checks if a player's total playtime in a given job fits within a given playtime range.
+///
+public sealed partial class PlayerJobPlaytimeRequirement : PlayerPlaytimeRequirement
+{
+ ///
+ /// The job we should check against the requirement.
+ ///
+ [DataField(required: true)]
+ public ProtoId Job = default!;
+
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ // Auto-pass if role timers are disabled.
+ if (ShouldAutoPass())
+ return true;
+
+ var playtime = GetJobPlaytime(context);
+ if (playtime is null)
+ return false;
+
+ return IsValidPlaytime(playtime.Value);
+ }
+
+ ///
+ public override string? GetReason()
+ {
+ // Do not give a reason if role timers are disabled.
+ if (ShouldAutoPass())
+ return null;
+
+ // Get the job name and format it with a color.
+ var protoMan = IoCManager.Resolve();
+ var jobName = FormatJob(protoMan);
+
+ // Get the playtime constraint string.
+ var playtimeString = GetPlaytimeConstraintReason();
+ if (playtimeString == null)
+ return null;
+
+ var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
+ ("inverted", Inverted),
+ ("timeConstraint", playtimeString));
+
+ // E.g. "You must have 2h30m of playtime as a Mime."
+ return Loc.GetString("player-requirement-job-playtime-reason",
+ ("constraint", constraintReason),
+ ("job", jobName));
+ }
+
+ ///
+ /// Format this requirement's job name, with a color.
+ ///
+ /// The prototype manager.
+ /// The department name of this prototype, formatted.
+ private string FormatJob(IPrototypeManager protoMan)
+ {
+ if (!protoMan.TryIndex(Job, out var job))
+ return Job;
+
+ var jobName = Loc.GetString(job.Name);
+
+ // Gotta use the department to recolor this role's name.
+ var entMan = IoCManager.Resolve();
+ var jobSystem = entMan.System();
+ var deptColor = Color.LightGray.ToHex();
+ if (jobSystem.TryGetPrimaryDepartment(Job, out var dept) || jobSystem.TryGetDepartment(Job, out dept))
+ deptColor = dept.Color.ToHex();
+
+ var formattedJob = Loc.GetString("player-requirement-format-job",
+ ("color", deptColor),
+ ("job", jobName));
+
+ return formattedJob;
+ }
+
+ ///
+ /// Get the total playtime for this job.
+ ///
+ /// A definition of parameters to check against the requirement.
+ /// The total playtime of this job. Null if either context playtimes or job is invalid.
+ private TimeSpan? GetJobPlaytime(PlayerRequirementContext context)
+ {
+ var protoMan = IoCManager.Resolve();
+ var playtime = TimeSpan.Zero;
+
+ if (context.Playtimes == null || !protoMan.TryIndex(Job, out var job))
+ return null;
+
+ if (context.Playtimes.TryGetValue(job.PlayTimeTracker, out var tracker))
+ playtime = tracker;
+
+ return playtime;
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
index e5fdb2b215..ee4511acc6 100644
--- a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
@@ -1,4 +1,5 @@
player-requirement-format-department = [color={$color}]{$department}[/color]
+player-requirement-format-job = [color={$color}]{$job}[/color]
player-requirement-format-species = [color=Green]{$species}[/color]
player-requirement-format-time = [color=Yellow]{$playtime}[/color]
player-requirement-format-trait = [color=LightBlue]{$trait}[/color]
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
index 9775b9a01d..d5516658ae 100644
--- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -4,6 +4,7 @@ player-requirement-playtime-minmax-time = between {$minimum} and {$maximum}
player-requirement-playtime-constraint-reason = Must{$inverted ->
[true] {" "}not
*[false] {""}
-} have {$timeConstraint}
+} have {$timeConstraint} of playtime
player-requirement-department-playtime-reason = {$constraint} in the {$department} department.
+player-requirement-job-playtime-reason = {$constraint} as a {$job}.
From e06f5d572900257d36a050684d921241664a1afc Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 19:04:44 -0500
Subject: [PATCH 28/54] Turn min/max ranges into IPlayerRangeRequirement
---
.../PlayerRequirements/CountRequirement.cs | 50 ++++-----
.../PlayerRequirements/IPlayerRequirement.cs | 74 +++++++++++++
.../PlayerRequirements.Playtime.cs | 102 +++++++++---------
.../_DEN/requirements/requirement-range.ftl | 7 +-
4 files changed, 154 insertions(+), 79 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
index 9c0c152b21..0f0a105f17 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -76,48 +76,50 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
///
/// To fulfill this requirement, you must have a number of items between two values, or either a minimum / maximum.
///
-public sealed partial class RangeCountRequirement : CountRequirement
+public sealed partial class RangeCountRequirement : CountRequirement, IPlayerRangeRequirement
{
///
- /// Minimum amount of required items you need.
+ /// Minimum amount of required items you need. Null means no minimum constraint.
///
[DataField]
- public int? Min = null;
+ public int? Min { get; set; } = null;
///
- /// Maximum amount of required items you can have.
+ /// Maximum amount of required items you can have. Null means no maximum constraint.
///
[DataField]
- public int? Max = null;
+ public int? Max { get; set; } = null;
///
public override string GetReason()
{
- return (Min, Max) switch
- {
- // "Must have between 1 and 5 of the following items."
- (not null, not null) => Loc.GetString("count-requirement-range-minmax-reason",
- ("minimum", Min),
- ("maximum", Max)),
-
- // "Must have at most 5 of the following items."
- (null, not null) => Loc.GetString("count-requirement-range-maximum-reason",
- ("maximum", Max)),
-
- // "Must have at least 1 of the following items."
- (not null, null) => Loc.GetString("count-requirement-range-minimum-reason",
- ("minimum", Min)),
-
- _ => string.Empty
- };
+ if (this is IPlayerRangeRequirement range)
+ return range.GetRangeConstraintReason();
+
+ return string.Empty;
}
///
public override bool CheckRequirement(IEnumerable have, IEnumerable required)
{
var count = GetFulfilledCount(have, required);
- return (Min == null || count >= Min)
- && (Max == null || count <= Max);
+
+ if (this is IPlayerRangeRequirement range)
+ return range.IsInRange(count);
+
+ return false;
+ }
+
+ ///
+ public string? GetMinText()
+ {
+ return Min?.ToString();
+ }
+
+ ///
+ public string? GetMaxText()
+ {
+ return Max?.ToString();
}
}
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 5aeae911d5..5180944eea 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Content.Shared._DEN.Requirements.Managers;
namespace Content.Shared._DEN.Requirements.PlayerRequirements;
@@ -93,3 +94,76 @@ public abstract partial class PlayerRequirement : IPlayerRequirement
///
public abstract bool PreCheck(PlayerRequirementContext context);
}
+
+///
+/// An interface representing a requirement where the value is between a given minimum
+/// and/or maximum range.
+///
+/// The value of the range to use.
+public interface IPlayerRangeRequirement where T : struct, IComparable
+{
+ ///
+ /// The optional minimum value of this requirement to pass.
+ ///
+ T? Min { get; set; }
+
+ ///
+ /// The optional maximum value of this requirement to pass.
+ ///
+ T? Max { get; set; }
+
+ ///
+ /// Check if a value is within range.
+ ///
+ /// The value to check.
+ /// Whether this value is in range or not.
+ bool IsInRange(T value)
+ {
+ // Value is less than minimum
+ if (Min != null && value.CompareTo(Min) < 0)
+ return false;
+
+ // Value is greater than maximum
+ if (Max != null && value.CompareTo(Max) > 0)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Get a reason message to display to the player for this requirement's allowed value range.
+ ///
+ string GetRangeConstraintReason()
+ {
+ var minText = GetMinText();
+ var maxText = GetMaxText();
+
+ return (minText, maxText) switch
+ {
+ // "Must have between 1 and 5 of the following items."
+ (not null, not null) => Loc.GetString("player-requirement-range-minmax-reason",
+ ("minimum", minText),
+ ("maximum", maxText)),
+
+ // "Must have at most 5 of the following items."
+ (null, not null) => Loc.GetString("player-requirement-range-maximum-reason",
+ ("maximum", maxText)),
+
+ // "Must have at least 1 of the following items."
+ (not null, null) => Loc.GetString("player-requirement-range-minimum-reason",
+ ("minimum", minText)),
+
+ _ => string.Empty
+ };
+ }
+
+ ///
+ /// Get a string representation of this range's minimum value.
+ ///
+ string? GetMinText();
+
+ ///
+ /// Get a string representation of this range's maximum value.
+ ///
+ string? GetMaxText();
+}
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 9f8f928061..9132cc2f7b 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.CCVar;
using Content.Shared.Localizations;
@@ -12,17 +13,19 @@ namespace Content.Shared._DEN.Requirements.PlayerRequirements;
/// An abstract class for playtime requirements that expect a playtime to be within
/// optional minimum and maximum parameters.
///
-public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement
+public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement, IPlayerRangeRequirement
{
///
/// The minimum time you can have in this tracker.
///
- [DataField] public TimeSpan? MinTime = null;
+ [DataField("minTime")]
+ public TimeSpan? Min { get; set; } = null;
///
/// The maximum time you can have in this tracker.
///
- [DataField] public TimeSpan? MaxTime = null;
+ [DataField("maxTime")]
+ public TimeSpan? Max { get; set; } = null;
///
public override bool PreCheck(PlayerRequirementContext context)
@@ -33,22 +36,6 @@ public override bool PreCheck(PlayerRequirementContext context)
return ShouldAutoPass() || context.Playtimes != null;
}
- ///
- /// Check if a given playtime tracker fits within the minimum and maximum times of this requirement.
- ///
- /// The playtime to check.
- /// Whether or not this playtime is valid.
- protected bool IsValidPlaytime(TimeSpan playtime)
- {
- if (MinTime != null & playtime < MinTime)
- return false;
-
- if (MaxTime != null & playtime > MaxTime)
- return false;
-
- return true;
- }
-
///
/// Whether or not this requirement should auto-pass. This applies if role timers
/// are disabled, because playtimes shouldn't matter anyway in this case - we shouldn't
@@ -64,45 +51,58 @@ protected static bool ShouldAutoPass()
}
///
- /// Gets a localized "reason" string for this requirement's playtime ranges.
+ /// Format a playtime TimeSpan into text to display to the player.
///
- ///
- /// For example: "Less than 30 minutes", "At least 120 minutes", "Between 20 minutes and 180 minutes".
- ///
- ///
- /// A string describing how much playtime you should have. Null if both minimum and maximum as null.
- ///
- protected string? GetPlaytimeConstraintReason()
+ /// The playtime to format.
+ /// The formatted playtime, if playtime is not null.
+ private static string? FormatPlaytime(TimeSpan? playtime)
{
- if (MinTime == null && MaxTime == null)
+ if (playtime is null)
return null;
- var minTimeString = FormatPlaytime(MinTime);
- var maxTimeString = FormatPlaytime(MaxTime);
+ var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value);
+ return Loc.GetString("player-requirement-format-time",
+ ("playtime", playtimeString));
+ }
- return (minTimeString, maxTimeString) switch
- {
- (not null, not null) => Loc.GetString("player-requirement-playtime-minmax-time",
- ("minimum", minTimeString), ("maximum", maxTimeString)),
+ ///
+ public string? GetMinText()
+ {
+ return FormatPlaytime(Min);
+ }
- (null, not null) => Loc.GetString("player-requirement-playtime-maximum-time",
- ("playtime", maxTimeString)),
+ ///
+ public string? GetMaxText()
+ {
+ return FormatPlaytime(Max);
+ }
- (not null, null) => Loc.GetString("player-requirement-playtime-minimum-time",
- ("playtime", minTimeString)),
+ ///
+ /// Check if the given playtime is in range.
+ ///
+ /// The playtime to check.
+ /// Whether or not the playtime is in range.
+ protected bool IsInRange(TimeSpan playtime)
+ {
+ if (this is IPlayerRangeRequirement range)
+ return range.IsInRange(playtime);
- _ => null
- };
+ return false;
}
- private static string? FormatPlaytime(TimeSpan? playtime)
+ ///
+ /// Get the text to display to the player that represents the range of valid playtimes.
+ ///
+ /// The playtime range description.
+ /// Whether or not this operation was successful.
+ protected bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString)
{
- if (playtime is null)
- return null;
+ playtimeString = null;
- var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value);
- return Loc.GetString("player-requirement-format-time",
- ("playtime", playtimeString));
+ if (this is IPlayerRangeRequirement range)
+ playtimeString = range.GetRangeConstraintReason();
+
+ return playtimeString != null;
}
}
@@ -128,7 +128,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
if (playtime is null)
return false;
- return IsValidPlaytime(playtime.Value);
+ return IsInRange(playtime.Value);
}
///
@@ -143,8 +143,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
var deptName = FormatDepartment(protoMan);
// Get the playtime constraint string.
- var playtimeString = GetPlaytimeConstraintReason();
- if (playtimeString == null)
+ if (!TryGetRangeConstraintReason(out var playtimeString))
return null;
var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
@@ -228,7 +227,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
if (playtime is null)
return false;
- return IsValidPlaytime(playtime.Value);
+ return IsInRange(playtime.Value);
}
///
@@ -243,8 +242,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
var jobName = FormatJob(protoMan);
// Get the playtime constraint string.
- var playtimeString = GetPlaytimeConstraintReason();
- if (playtimeString == null)
+ if (!TryGetRangeConstraintReason(out var playtimeString))
return null;
var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
index 63b2c095c6..2a88288b75 100644
--- a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
@@ -1,6 +1,7 @@
+player-requirement-range-minimum-reason = at least {$minimum}
+player-requirement-range-maximum-reason = at most {$maximum}
+player-requirement-range-minmax-reason = between {$minimum} and {$maximum}
+
count-requirement-constant-reason = exactly {$count}
-count-requirement-range-minimum-reason = at least {$minimum}
-count-requirement-range-maximum-reason = at most {$maximum}
-count-requirement-range-minmax-reason = between {$minimum} and {$maximum}
count-requirement-any-reason = any
count-requirement-all-reason = all
From e2b0805f4fa77f607a00f89b199dfed87e75b37e Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 19:28:10 -0500
Subject: [PATCH 29/54] add age requirement
---
.../PlayerRequirements.Profile.cs | 122 ++++++++++++++----
.../requirements/profile-requirements.ftl | 5 +
2 files changed, 104 insertions(+), 23 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index 2fbfd17e05..f0aad45a13 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Traits.Prototypes;
@@ -7,18 +8,21 @@
namespace Content.Shared._DEN.Requirements.PlayerRequirements;
///
-/// Checks if a player's character has a required number of the given traits.
+/// Checks if a player's character is within the given age range.
///
-public sealed partial class PlayerTraitRequirement : PlayerRequirement
+public sealed partial class PlayerAgeRequirement : PlayerRequirement, IPlayerRangeRequirement
{
///
- /// Traits that the character needs to have to the pass the requirement.
+ /// Minimum age of the character to pass the requirement.
///
[DataField]
- public HashSet> Traits = new();
+ public int? Min { get; set; } = null;
+ ///
+ /// Maximum age of the character to pass the requirement.
+ ///
[DataField]
- public CountRequirement Count;
+ public int? Max { get; set; } = null;
///
public override bool PreCheck(PlayerRequirementContext context)
@@ -32,38 +36,49 @@ public override bool CheckRequirement(PlayerRequirementContext context)
if (context.Profile == null)
return false;
- var profileTraits = context.Profile.EntityTraitPreferences;
- return Count.CheckRequirement(profileTraits, Traits);
+ if (this is IPlayerRangeRequirement range)
+ return range.IsInRange(context.Profile.Age);
+
+ return false;
}
///
public override string? GetReason()
{
- var protoMan = IoCManager.Resolve();
- var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan));
- var traitList = string.Join(", ", traitNames);
- var constraintReason = Count.GetReason();
+ if (!TryGetRangeConstraintReason(out var constraintReason))
+ return null;
- return Loc.GetString("player-requirement-trait-reason",
+ // "Must be between 20 and 40 years old."
+ return Loc.GetString("player-requirement-age-reason",
("inverted", Inverted),
- ("constraint", constraintReason),
- ("traits", traitList));
+ ("constraint", constraintReason));
}
///
- /// Localizes a trait ID into a formatted trait name.
+ /// Get the text to display to the player that represents the range of valid playtimes.
///
- /// The ID of the trait.
- /// The prototype manager.
- /// A formatted trait name string.
- private static string LocalizeTrait(ProtoId traitId, IPrototypeManager protoMan)
+ /// The playtime range description.
+ /// Whether or not this operation was successful.
+ private bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? reason)
{
- var traitName = traitId;
+ reason = null;
- if (protoMan.TryIndex(traitId, out var trait))
- traitName = Loc.GetString(trait.Name);
+ if (this is IPlayerRangeRequirement range)
+ reason = range.GetRangeConstraintReason();
- return Loc.GetString("player-requirement-format-trait", ("trait", traitName));
+ return reason != null;
+ }
+
+ ///
+ public string? GetMinText()
+ {
+ return Min?.ToString();
+ }
+
+ ///
+ public string? GetMaxText()
+ {
+ return Max?.ToString();
}
}
@@ -127,3 +142,64 @@ private static string LocalizeSpecies(ProtoId speciesId, IProt
return Loc.GetString("player-requirement-format-species", ("species", speciesName));
}
}
+
+///
+/// Checks if a player's character has a required number of the given traits.
+///
+public sealed partial class PlayerTraitRequirement : PlayerRequirement
+{
+ ///
+ /// Traits that the character needs to have to the pass the requirement.
+ ///
+ [DataField]
+ public HashSet> Traits = new();
+
+ [DataField]
+ public CountRequirement Count;
+
+ ///
+ public override bool PreCheck(PlayerRequirementContext context)
+ {
+ return context.Profile != null;
+ }
+
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ if (context.Profile == null)
+ return false;
+
+ var profileTraits = context.Profile.EntityTraitPreferences;
+ return Count.CheckRequirement(profileTraits, Traits);
+ }
+
+ ///
+ public override string? GetReason()
+ {
+ var protoMan = IoCManager.Resolve();
+ var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan));
+ var traitList = string.Join(", ", traitNames);
+ var constraintReason = Count.GetReason();
+
+ return Loc.GetString("player-requirement-trait-reason",
+ ("inverted", Inverted),
+ ("constraint", constraintReason),
+ ("traits", traitList));
+ }
+
+ ///
+ /// Localizes a trait ID into a formatted trait name.
+ ///
+ /// The ID of the trait.
+ /// The prototype manager.
+ /// A formatted trait name string.
+ private static string LocalizeTrait(ProtoId traitId, IPrototypeManager protoMan)
+ {
+ var traitName = traitId;
+
+ if (protoMan.TryIndex(traitId, out var trait))
+ traitName = Loc.GetString(trait.Name);
+
+ return Loc.GetString("player-requirement-format-trait", ("trait", traitName));
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
index 02bce0b096..f2799cb22c 100644
--- a/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/profile-requirements.ftl
@@ -7,3 +7,8 @@ player-requirement-species-reason = Must{$inverted ->
[true] {" "}not
*[false] {""}
} be one of these species: {$species}
+
+player-requirement-age-reason = Must{$inverted ->
+ [true] {" "}not
+ *[false] {""}
+} be {$constraint} years old.
From 5a9e35d9000a19717dcf8044c68eb91054a8c852 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Wed, 25 Mar 2026 20:04:23 -0500
Subject: [PATCH 30/54] add overall playtime requirement
---
.../PlayerRequirements/IPlayerRequirement.cs | 1 -
.../PlayerRequirements.Playtime.cs | 60 +++++++++++++++++++
.../requirements/playtime-requirements.ftl | 1 +
3 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 5180944eea..a1cc4bb623 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -1,4 +1,3 @@
-using System.Numerics;
using Content.Shared._DEN.Requirements.Managers;
namespace Content.Shared._DEN.Requirements.PlayerRequirements;
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 9132cc2f7b..3cdc3ee4ac 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -2,6 +2,7 @@
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.CCVar;
using Content.Shared.Localizations;
+using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
@@ -300,3 +301,62 @@ private string FormatJob(IPrototypeManager protoMan)
return playtime;
}
}
+
+///
+/// Checks if a player's total overall playtime fits within a given playtime range.
+///
+public sealed partial class PlayerOverallPlaytimeRequirement : PlayerPlaytimeRequirement
+{
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ // Auto-pass if role timers are disabled.
+ if (ShouldAutoPass())
+ return true;
+
+ var playtime = GetOverallPlaytime(context);
+ if (playtime is null)
+ return false;
+
+ return IsInRange(playtime.Value);
+ }
+
+ ///
+ /// Get the overall playtime for this context.
+ ///
+ /// The context being used for checking this requirement.
+ /// The player's overall playtime.
+ private static TimeSpan? GetOverallPlaytime(PlayerRequirementContext context)
+ {
+ var overallTracker = PlayTimeTrackingShared.TrackerOverall;
+ var playtime = TimeSpan.Zero;
+
+ if (context.Playtimes == null)
+ return null;
+
+ if (context.Playtimes.TryGetValue(overallTracker, out var tracker))
+ playtime = tracker;
+
+ return playtime;
+ }
+
+ ///
+ public override string? GetReason()
+ {
+ // Do not give a reason if role timers are disabled.
+ if (ShouldAutoPass())
+ return null;
+
+ // Get the playtime constraint string.
+ if (!TryGetRangeConstraintReason(out var playtimeString))
+ return null;
+
+ var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
+ ("inverted", Inverted),
+ ("timeConstraint", playtimeString));
+
+ // E.g. "You must have 300h of playtime overall."
+ return Loc.GetString("player-requirement-overall-playtime-reason",
+ ("constraint", constraintReason));
+ }
+}
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
index d5516658ae..64bbd8ba84 100644
--- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -8,3 +8,4 @@ player-requirement-playtime-constraint-reason = Must{$inverted ->
player-requirement-department-playtime-reason = {$constraint} in the {$department} department.
player-requirement-job-playtime-reason = {$constraint} as a {$job}.
+player-requirement-overall-playtime-reason = {$constraint} overall.
From 23d9543d5a4aa937bfcec8b6cea70b0f14d72d65 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 12:51:50 -0500
Subject: [PATCH 31/54] obsolete more job requirements
---
Content.Shared/Roles/JobRequirement/AgeRequirement.cs | 1 +
.../Roles/JobRequirement/DepartmentTimeRequirement.cs | 1 +
.../Roles/JobRequirement/OverallPlaytimeRequirement.cs | 1 +
Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs | 1 +
Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs | 1 +
Content.Shared/Roles/JobRequirement/TraitsRequirement.cs | 2 +-
.../_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs | 1 +
7 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs
index 30f607adf7..a7106484df 100644
--- a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs
+++ b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs
@@ -12,6 +12,7 @@ namespace Content.Shared.Roles;
///
[UsedImplicitly]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerAgeRequirement instead")] // DEN
public sealed partial class AgeRequirement : JobRequirement
{
[DataField(required: true)]
diff --git a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs
index 4034b8b419..a05cb952f9 100644
--- a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs
+++ b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs
@@ -10,6 +10,7 @@ namespace Content.Shared.Roles;
[UsedImplicitly]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerDepartmentPlaytimeRequirement instead")] // DEN
public sealed partial class DepartmentTimeRequirement : JobRequirement
{
///
diff --git a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs
index 67b3938e1a..543f16b3b9 100644
--- a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs
+++ b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs
@@ -11,6 +11,7 @@ namespace Content.Shared.Roles;
[UsedImplicitly]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerOverallPlaytimeRequirement instead")] // DEN
public sealed partial class OverallPlaytimeRequirement : JobRequirement
{
///
diff --git a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs
index f096cfcb42..c5f6fce5ec 100644
--- a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs
+++ b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs
@@ -12,6 +12,7 @@ namespace Content.Shared.Roles;
[UsedImplicitly]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerJobPlaytimeRequirement instead")] // DEN
public sealed partial class RoleTimeRequirement : JobRequirement
{
///
diff --git a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs
index 68c069931f..736247c5fa 100644
--- a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs
+++ b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs
@@ -14,6 +14,7 @@ namespace Content.Shared.Roles;
///
[UsedImplicitly]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerSpeciesRequirement instead")] // DEN
public sealed partial class SpeciesRequirement : JobRequirement
{
[DataField(required: true)]
diff --git a/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs b/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs
index 3ebba44cf2..a1016c107e 100644
--- a/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs
+++ b/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs
@@ -15,7 +15,7 @@ namespace Content.Shared.Roles;
///
[UsedImplicitly]
[Serializable, NetSerializable]
-[Obsolete("Use EntityTraitsRequirement instead")] // DEN
+[Obsolete("Use PlayerTraitRequirement instead")] // DEN
public sealed partial class TraitsRequirement : JobRequirement
{
[DataField(required: true)]
diff --git a/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs b/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs
index ad813ecd44..965db85379 100644
--- a/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs
+++ b/Content.Shared/_DEN/Roles/JobRequirement/EntityTraitsRequirement.cs
@@ -15,6 +15,7 @@ namespace Content.Shared._DEN.Roles;
///
[UsedImplicitly]
[Serializable, NetSerializable]
+[Obsolete("Use PlayerTraitRequirement instead")]
public sealed partial class EntityTraitsRequirement : JobRequirement
{
[DataField(required: true)]
From df0bb0a3da2d826726d6019c784c727ddac72c0a Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 12:53:14 -0500
Subject: [PATCH 32/54] move this out of IPlayerRequirementManager
---
.../Managers/IPlayerRequirementManager.cs | 17 ----------------
.../SharedPlayerRequirementManager.cs | 20 +++++++++++++++----
2 files changed, 16 insertions(+), 21 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
index 297d3b03f6..e0ac562b06 100644
--- a/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/IPlayerRequirementManager.cs
@@ -9,23 +9,6 @@ namespace Content.Shared._DEN.Requirements.Managers;
///
public interface IPlayerRequirementManager
{
- ///
- /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements.
- ///
- /// The context containing fields to check against the requirements.
- /// An enumerable collection of requirements.
- /// Whether or not this context passes *all* requirements. If even one fails, then this is false.
- bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements);
-
- ///
- /// Whether or not the item associated with a set of requirements should be hidden
- /// from the player, such as in UI.
- ///
- /// The context containing fields to check against the requirements.
- /// An enumerable collection of requirements.
- /// Whether or not the item should be hidden from the player.
- bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements);
-
///
/// Creates a new PlayerRequirementContext with context fields pre-filled.
///
diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
index 3205637ae7..adbfef012f 100644
--- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
@@ -10,9 +10,14 @@ namespace Content.Shared._DEN.Requirements.Managers;
///
public abstract partial class SharedPlayerRequirementManager : IPlayerRequirementManager
{
- ///
+ ///
+ /// Check a context against enumerable requirements and gets the final pass/fail status of these requirements.
+ ///
+ /// The context containing fields to check against the requirements.
+ /// An enumerable collection of requirements.
+ /// Whether or not this context passes *all* requirements. If even one fails, then this is false.
[PublicAPI]
- public bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements)
+ public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable requirements)
{
foreach (var requirement in requirements)
if (!CheckRequirement(context, requirement))
@@ -21,9 +26,16 @@ public bool CheckRequirements(PlayerRequirementContext context, IEnumerable
+ ///
+ /// Whether or not the item associated with a set of requirements should be hidden
+ /// from the player, such as in UI.
+ ///
+ /// The context containing fields to check against the requirements.
+ /// An enumerable collection of requirements.
+ /// Whether or not the item should be hidden from the player.
+
[PublicAPI]
- public bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements)
+ public static bool ShouldHide(PlayerRequirementContext context, IEnumerable requirements)
{
foreach (var requirement in requirements)
if (!CheckRequirement(context, requirement) && requirement.HideIfFailed)
From ac4282f977921349c1517bff4f42e8b1f927976a Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 12:54:52 -0500
Subject: [PATCH 33/54] use static CheckRequirements
---
Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
index ae503dc1d4..b1281a40d1 100644
--- a/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
+++ b/Content.Server/_DEN/Traits/EntitySystems/TraitSystem.cs
@@ -1,7 +1,6 @@
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Traits.EntitySystems;
using Content.Shared.GameTicking;
-using Content.Shared.Humanoid;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
@@ -46,7 +45,7 @@ private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
var context = _requirements.GetPlayerContext(args.Player);
context.Profile = args.Profile;
- if (!_requirements.CheckRequirements(context, trait.Requirements))
+ if (!SharedPlayerRequirementManager.CheckRequirements(context, trait.Requirements))
{
Log.Error($"Tried to spawn trait {traitId} on {ToPrettyString(mob)}, but we failed the requirements to do so!");
continue;
From 1dab83d66f1e845e1ace9bef533a4fc152fac805 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 13:06:51 -0500
Subject: [PATCH 34/54] add PlayerRequirementLoadoutEffect
---
.../Effects/JobRequirementLoadoutEffect.cs | 1 +
.../Effects/PlayerRequirementLoadoutEffect.cs | 47 +++++++++++++++++++
2 files changed, 48 insertions(+)
create mode 100644 Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
diff --git a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs
index b9bf4b38bc..e793cd7e85 100644
--- a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs
+++ b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs
@@ -12,6 +12,7 @@ namespace Content.Shared.Preferences.Loadouts.Effects;
///
/// Checks for a job requirement to be met such as playtime.
///
+[Obsolete("Use PlayerRequirementLoadoutEffect")] // DEN
public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
new file mode 100644
index 0000000000..c3f75fa0c0
--- /dev/null
+++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared._DEN.Requirements.PlayerRequirements;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Shared._DEN.Preferences.Loadouts.Effects;
+
+///
+/// Checks for a player requirement to be met.
+///
+public sealed partial class PlayerRequirementLoadoutEffect : LoadoutEffect
+{
+ [DataField(required: true)]
+ public IPlayerRequirement Requirement = default!;
+
+ public override bool Validate(HumanoidCharacterProfile profile,
+ RoleLoadout loadout,
+ ICommonSession? session,
+ IDependencyCollection collection,
+ [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ if (session == null)
+ {
+ reason = FormattedMessage.Empty;
+ return false;
+ }
+
+ var requirements = collection.Resolve();
+ var context = requirements.GetPlayerContext(session);
+ context.Profile = profile;
+ var success = SharedPlayerRequirementManager.CheckRequirement(context, Requirement);
+
+ if (!success)
+ {
+ var reasonText = Requirement.GetReason() ?? string.Empty;
+ reason = FormattedMessage.FromMarkupPermissive(reasonText);
+ return false;
+ }
+
+ reason = null;
+ return true;
+ }
+}
From 63f43f892bb0f3e52891570f60dfb735dd3ecc49 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 13:23:03 -0500
Subject: [PATCH 35/54] static ShouldHide
---
Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
index 2d9c28351b..ba49b0bb79 100644
--- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
+++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
@@ -67,7 +67,7 @@ private void UpdateVisibility(EntityTraitPrototype trait)
return;
var context = _requirements.GetPlayerContext(session);
- Visible = !_requirements.ShouldHide(context, trait.Requirements);
+ Visible = !SharedPlayerRequirementManager.ShouldHide(context, trait.Requirements);
}
///
From 8a640ccd5d832b1c4aa0f85f91df62f26edbb8e9 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 13:23:59 -0500
Subject: [PATCH 36/54] oops i should disable timers here
---
.../Loadouts/Effects/PlayerRequirementLoadoutEffect.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
index c3f75fa0c0..4c2d22c9ce 100644
--- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
+++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
@@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Requirements.PlayerRequirements;
+using Content.Shared.CCVar;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
+using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -23,7 +25,10 @@ public override bool Validate(HumanoidCharacterProfile profile,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{
- if (session == null)
+ var configurationManager = collection.Resolve();
+ var timersDisabled = !configurationManager.GetCVar(CCVars.GameRoleLoadoutTimers);
+
+ if (session == null || timersDisabled)
{
reason = FormattedMessage.Empty;
return false;
From f5822a82290aa433565374f58aae76f9486225c0 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 16:48:58 -0500
Subject: [PATCH 37/54] add playtime requirement types
---
.../Effects/PlayerRequirementLoadoutEffect.cs | 9 +++----
.../PlayerRequirements.Playtime.cs | 25 +++++++++++++++++--
2 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
index 4c2d22c9ce..269e323c67 100644
--- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
+++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
@@ -1,11 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared._DEN.Requirements.PlayerRequirements;
-using Content.Shared.CCVar;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
-using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -25,10 +23,9 @@ public override bool Validate(HumanoidCharacterProfile profile,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{
- var configurationManager = collection.Resolve();
- var timersDisabled = !configurationManager.GetCVar(CCVars.GameRoleLoadoutTimers);
-
- if (session == null || timersDisabled)
+ if (session == null
+ // Auto-pass playtime requirements if they're disabled
+ || Requirement is PlayerPlaytimeRequirement playtimeReq && playtimeReq.ShouldAutoPass())
{
reason = FormattedMessage.Empty;
return false;
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 3cdc3ee4ac..23baf5403a 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -28,6 +28,13 @@ public abstract partial class PlayerPlaytimeRequirement : PlayerRequirement, IPl
[DataField("maxTime")]
public TimeSpan? Max { get; set; } = null;
+ ///
+ /// The "type" of playtime requirement this is.
+ /// This affects what CVAR is used to turn the timer off.
+ ///
+ [DataField]
+ public PlaytimeRequirementType RequirementType = PlaytimeRequirementType.Role;
+
///
public override bool PreCheck(PlayerRequirementContext context)
{
@@ -45,10 +52,17 @@ public override bool PreCheck(PlayerRequirementContext context)
///
/// Whether or not this requirement should auto-pass.
///
- protected static bool ShouldAutoPass()
+ public bool ShouldAutoPass()
{
var config = IoCManager.Resolve();
- return !config.GetCVar(CCVars.GameRoleTimers);
+ var timerEnabled = RequirementType switch
+ {
+ PlaytimeRequirementType.Role => config.GetCVar(CCVars.GameRoleTimers),
+ PlaytimeRequirementType.Loadout => config.GetCVar(CCVars.GameRoleLoadoutTimers),
+ _ => throw new ArgumentOutOfRangeException(nameof(RequirementType)),
+ };
+
+ return !timerEnabled;
}
///
@@ -107,6 +121,13 @@ protected bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playt
}
}
+[Serializable]
+public enum PlaytimeRequirementType
+{
+ Role,
+ Loadout
+}
+
///
/// Checks if a player's total playtime in a given department fits within a given playtime range.
///
From 33c61bf654dabdebe132cfc8e8048780bd05215d Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 16:53:37 -0500
Subject: [PATCH 38/54] use player requirements in loadouts
---
.../Loadouts/Jobs/Cargo/cargo_technician.yml | 45 +++++++----
.../Loadouts/Jobs/Cargo/quartermaster.yml | 17 ++--
.../Loadouts/Jobs/Civilian/bartender.yml | 17 ++--
.../Loadouts/Jobs/Civilian/chaplain.yml | 53 +++++++++----
.../Loadouts/Jobs/Civilian/janitor.yml | 17 ++--
.../Loadouts/Jobs/Civilian/passenger.yml | 34 +++++---
.../Loadouts/Jobs/Command/captain.yml | 17 ++--
.../Jobs/Command/head_of_personnel.yml | 34 +++++---
.../Jobs/Engineering/chief_engineer.yml | 17 ++--
.../Jobs/Engineering/station_engineer.yml | 45 +++++++----
.../Loadouts/Jobs/Medical/role_timers.yml | 79 +++++++++++++------
.../Jobs/Science/research_director.yml | 17 ++--
.../Loadouts/Jobs/Science/scientist.yml | 15 +++-
.../Jobs/Security/head_of_security.yml | 17 ++--
.../Jobs/Security/security_officer.yml | 30 ++++---
.../Loadouts/Jobs/Wildcards/reporter.yml | 34 +++++---
.../Loadouts/Miscellaneous/glasses.yml | 34 +++++---
.../Loadouts/Miscellaneous/jobtrinkets.yml | 34 +++++---
.../Loadouts/Miscellaneous/trinkets.yml | 76 ++++++++++++------
19 files changed, 443 insertions(+), 189 deletions(-)
diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml
index d23027130f..069dceded8 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml
@@ -2,21 +2,38 @@
- type: loadoutEffectGroup
id: SeniorCargo
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobCargoTechnician
- time: 21600 #6 hrs
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobSalvageSpecialist
- time: 21600 #6 hrs
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: CargoTechnician
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: SalvageSpecialist
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Cargo
- time: 216000 # 60 hrs
+ minTime: 60h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobCargoTechnician
+ # time: 21600 #6 hrs
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobSalvageSpecialist
+ # time: 21600 #6 hrs
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Cargo
+ # time: 216000 # 60 hrs
+ # End DEN
# Head
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
index 602b2d36fd..1b7c4bf524 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
@@ -2,11 +2,18 @@
- type: loadoutEffectGroup
id: MasterQM
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobQuartermaster
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: QuarterMaster
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobQuartermaster
+ # time: 20h
+ # End DEN
# Jumpsuit
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml
index b8a8744915..b928dd64e1 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml
@@ -1,11 +1,18 @@
- type: loadoutEffectGroup
id: SeniorBar
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobBartender
- time: 52h # 1 hour per week for 1 year
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Bartender
+ minTime: 52h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobBartender
+ # time: 52h # 1 hour per week for 1 year
+ # End DEN
# Head
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml
index 0bfc99d41f..1ab03e734a 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml
@@ -2,31 +2,52 @@
- type: loadoutEffectGroup
id: NanoTrasenBibleRequirement
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobCaptain
- time: 7200 #2 hrs
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Captain
+ minTime: 2h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobCaptain
+ # time: 7200 #2 hrs
+ # End DEN
# Playtime requirement for Druid Bible, Druidic Tablet
- type: loadoutEffectGroup
id: DruidBibleRequirement
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobBotanist
- time: 18000 #5 hrs
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Botanist
+ minTime: 5h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobBotanist
+ # time: 18000 #5 hrs
+ # End DEN
# Playtime requirement for Clown Bible, Mirth of the Honkmother
- type: loadoutEffectGroup
id: ClownBibleRequirement
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobClown
- time: 18000 #5 hrs
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Clown
+ minTime: 5h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobClown
+ # time: 18000 #5 hrs
+ # End DEN
# Head
- type: loadout
@@ -132,7 +153,7 @@
storage:
back:
- BibleNanoTrasen
-
+
- type: loadout
id: BibleNarsie
storage:
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml
index 431b83a99d..02df8d61ec 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml
@@ -1,11 +1,18 @@
- type: loadoutEffectGroup
id: SeniorJanitorial
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobJanitor
- time: 52h # 1 hour per week for 1 year
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Janitor
+ minTime: 52h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobJanitor
+ # time: 52h # 1 hour per week for 1 year
+ # End DEN
# Head
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml
index 1ed5e8aca0..c9ac6729ee 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml
@@ -2,21 +2,35 @@
- type: loadoutEffectGroup
id: GreyTider
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobPassenger
- time: 10h # silly reward for people who play passenger a lot
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Passenger
+ minTime: 10h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobPassenger
+ # time: 10h # silly reward for people who play passenger a lot
+ # End DEN
# Head of Greytide (for grey mantle)
- type: loadoutEffectGroup
id: MasterGrey
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobPassenger
- time: 20h # fun mantle for the most experienced greytiders
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Passenger
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobPassenger
+ # time: 20h # fun mantle for the most experienced greytiders
+ # End DEN
# Face
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml b/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml
index 2951678968..62b4920f18 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml
@@ -2,11 +2,18 @@
- type: loadoutEffectGroup
id: MasterCap
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobCaptain
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Captain
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobCaptain
+ # time: 20h
+ # End DEN
# Jumpsuit
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml
index 17a84b7386..3bc5952afa 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml
@@ -2,21 +2,35 @@
- type: loadoutEffectGroup
id: MasterHoP
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobHeadOfPersonnel
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: HeadOfPersonnel
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobHeadOfPersonnel
+ # time: 20h
+ # End DEN
# Professional HoP Time
- type: loadoutEffectGroup
id: ProfessionalHoP
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobHeadOfPersonnel
- time: 15h # special reward for HoP mains
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: HeadOfPersonnel
+ minTime: 15h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobHeadOfPersonnel
+ # time: 15h # special reward for HoP mains
+ # End DEN
# Jumpsuit
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml
index 55f184a168..d2d313d627 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml
@@ -2,11 +2,18 @@
- type: loadoutEffectGroup
id: MasterCE
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobChiefEngineer
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: ChiefEngineer
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobChiefEngineer
+ # time: 20h
+ # End DEN
# Jumpsuit
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
index 64bfe79a4d..9c018b2725 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
@@ -2,21 +2,38 @@
- type: loadoutEffectGroup
id: SeniorEngineering
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobAtmosphericTechnician
- time: 6h
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobStationEngineer
- time: 6h
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: AtmosphericsTechnician
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: StationEngineer
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Engineering
- time: 60h
+ minTime: 60h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobAtmosphericTechnician
+ # time: 6h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobStationEngineer
+ # time: 6h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 60h
+ # End DEN
# Head
- type: startingGear
diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml
index f023cfc227..cc5eadd03f 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Medical/role_timers.yml
@@ -2,38 +2,69 @@
- type: loadoutEffectGroup
id: SeniorPhysician
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobChemist
- time: 6h
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobMedicalDoctor
- time: 6h
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Chemist
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: MedicalDoctor
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Medical
- time: 60h
+ minTime: 60h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobChemist
+ # time: 6h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobMedicalDoctor
+ # time: 6h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 60h
+ # End DEN
# Mid-level timer for players who have a solid amount of experience
- type: loadoutEffectGroup
id: MedicalJourneymanTimer
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobMedicalDoctor
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: MedicalDoctor
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobMedicalDoctor
+ # time: 20h
+ # End DEN
# Mid-level timer for CMO
- type: loadoutEffectGroup
id: MasterCMO
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobChiefMedicalOfficer
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: ChiefMedicalOfficer
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobChiefMedicalOfficer
+ # time: 20h
+ # End DEN
diff --git a/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml b/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml
index 3717c1c67c..a325320db0 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml
@@ -2,11 +2,18 @@
- type: loadoutEffectGroup
id: MasterRD
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobResearchDirector
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: ResearchDirector
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobResearchDirector
+ # time: 20h
+ # End DEN
# Head
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml b/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml
index 080015f0da..9b1591f9d9 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml
@@ -2,11 +2,18 @@
- type: loadoutEffectGroup
id: SeniorResearcher
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Science
- time: 60h
+ minTime: 60h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Science
+ # time: 60h
+ # End DEN
# Head
- type: startingGear
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml
index e85e1c8ccb..89c8694875 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml
@@ -2,11 +2,18 @@
- type: loadoutEffectGroup
id: MasterHoS
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobHeadOfSecurity
- time: 20h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: HeadOfSecurity
+ minTime: 20h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobHeadOfSecurity
+ # time: 20h
+ # End DEN
# Jumpsuit
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml b/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml
index e0b0ce1a3f..6e2c90e9ab 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml
@@ -2,16 +2,28 @@
- type: loadoutEffectGroup
id: SeniorOfficer
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobWarden
- time: 6h
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Warden
+ minTime: 6h
+ requirementType: Loadout
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Security
- time: 60h
+ minTime: 60h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobWarden
+ # time: 6h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 60h
+ # End DEN
# Head
- type: loadout
diff --git a/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml b/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml
index f5f9825526..85f08d0a61 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml
@@ -13,11 +13,18 @@
- type: loadout
id: ReporterPressFedora
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobReporter
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Reporter
+ minTime: 10h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobReporter
+ # time: 10h
+ # End DEN
equipment:
head: ClothingHeadHatFedoraPress
@@ -25,10 +32,17 @@
- type: loadout
id: ReporterPressVest
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobReporter
- time: 35h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Reporter
+ minTime: 35h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobReporter
+ # time: 35h
+ # End DEN
equipment:
outerClothing: ClothingOuterVestPress
diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
index 8a474d359a..ed80248fb7 100644
--- a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
+++ b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
@@ -2,20 +2,34 @@
- type: loadoutEffectGroup
id: JamjarTimer
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobLibrarian
- time: 1h # for being the biggest nerd on the station
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Librarian
+ minTime: 1h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobLibrarian
+ # time: 1h # for being the biggest nerd on the station
+ # End DEN
- type: loadoutEffectGroup
id: JensenTimer
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
- department: Cargo
- time: 10h # 10 hours of being a space trucker
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Cargo
+ minTime: 10h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Cargo
+ # time: 10h # 10 hours of being a space trucker
+ # End DEN
# Basic options
# Glasses
diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml
index 27d266c5bb..173cb7d087 100644
--- a/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml
+++ b/Resources/Prototypes/Loadouts/Miscellaneous/jobtrinkets.yml
@@ -12,11 +12,18 @@
- type: loadout
id: FlowerWaterClown
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobClown
- time: 4h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Clown
+ minTime: 4h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobClown
+ # time: 4h
+ # End DEN
storage:
back:
- SprayFlowerPin
@@ -24,11 +31,18 @@
- type: loadout
id: SecStar
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
- department: Security
- time: 100h
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 100h
+ requirementType: Loadout
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 100h
+ # End DEN
storage:
back:
- Dinkystar
diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
index 50331ff89d..ccb084300f 100644
--- a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
+++ b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
@@ -2,11 +2,17 @@
- type: loadoutEffectGroup
id: Command
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Command
- time: 1h
+ minTime: 1h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Command
+ # time: 1h
+ # End DEN
# Flowers
- type: loadout
@@ -277,11 +283,17 @@
- type: loadout
id: OffsetCaneClown
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobClown
- time: 3600 # 1hr
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Clown
+ minTime: 1h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobClown
+ # time: 3600 # 1hr
+ # End DEN
storage:
back:
- OffsetCaneClown
@@ -290,11 +302,17 @@
- type: loadout
id: OffsetCaneMime
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobMime
- time: 3600 # 1hr
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Mime
+ minTime: 1h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobMime
+ # time: 3600 # 1hr
+ # End DEN
storage:
back:
- OffsetCaneMime
@@ -303,11 +321,17 @@
- type: loadout
id: OffsetCaneNT
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Command
- time: 18000 # 5hr
+ minTime: 5h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:DepartmentTimeRequirement
+ # department: Command
+ # time: 18000 # 5hr
+ # End DEN
storage:
back:
- OffsetCaneNT
@@ -316,11 +340,17 @@
- type: loadout
id: Cane
effects:
- - !type:JobRequirementLoadoutEffect
- requirement:
- !type:RoleTimeRequirement
- role: JobLibrarian
- time: 3600 # 1hr same as Jamjar.
+ # Begin DEN: Use PlayerRequirements
+ - !type:PlayerRequirementLoadoutEffect
+ requirement: !type:PlayerJobPlaytimeRequirement
+ job: Librarian
+ minTime: 1h
+ # - !type:JobRequirementLoadoutEffect
+ # requirement:
+ # !type:RoleTimeRequirement
+ # role: JobLibrarian
+ # time: 3600 # 1hr same as Jamjar.
+ # End DEN
storage:
back:
- Cane
From 921dea673bbcaaa86e2b873091bd260e6633712c Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 18:16:45 -0500
Subject: [PATCH 39/54] obsoleted and refactored a lot of bullshsit
---
.../JobRequirementsManager.cs | 27 ++++++-
.../PlayTimeTracking/JobRequirementManager.cs | 30 +++++++
.../PlayTimeTrackingSystem.cs | 72 +++++++++++------
.../PlayTimeTrackingSystem.cs | 40 ++++++++++
Content.Shared/Roles/AntagPrototype.cs | 10 +++
Content.Shared/Roles/JobPrototype.cs | 10 +++
.../Roles/JobRequirementOverridePrototype.cs | 19 +++++
Content.Shared/Roles/SharedRoleSystem.cs | 6 +-
.../SharedPlayerRequirementManager.cs | 39 +++++++++
Content.Shared/_DEN/Roles/SharedRoleSystem.cs | 80 +++++++++++++++++++
10 files changed, 308 insertions(+), 25 deletions(-)
create mode 100644 Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
create mode 100644 Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
create mode 100644 Content.Shared/_DEN/Roles/SharedRoleSystem.cs
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index 9325507c53..94edcc3746 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist;
@@ -15,7 +16,7 @@
namespace Content.Client.Players.PlayTimeTracking;
-public sealed class JobRequirementsManager : ISharedPlaytimeManager
+public sealed partial class JobRequirementsManager : ISharedPlaytimeManager // DEN: Make partial
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
@@ -23,6 +24,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
+ [Dependency] private readonly IPlayerRequirementManager _requirements = default!; // DEN
private readonly Dictionary _roles = new();
private readonly List> _jobBans = new();
@@ -148,10 +150,21 @@ public bool IsAllowed(
return false;
// Check other role requirements
+ // TODO DEN: This is deprecated
var reqs = _entManager.System().GetRoleRequirements(job);
if (!CheckRoleRequirements(reqs, profile, out reason))
return false;
+ // Begin DEN: Use player requirements
+ var roleSystem = _entManager.System();
+ var requirements = roleSystem.GetRolePlayerRequirements(job);
+ if (requirements != null && !PassesRequirements(profile, requirements))
+ {
+ reason = SharedPlayerRequirementManager.GetCombinedReason(requirements);
+ return false;
+ }
+ // End DEN
+
return true;
}
@@ -175,14 +188,26 @@ public bool IsAllowed(
return false;
// Check other role requirements
+ // TODO DEN: This is deprecated
var reqs = _entManager.System().GetRoleRequirements(antag);
if (!CheckRoleRequirements(reqs, profile, out reason))
return false;
+ // Begin DEN: Use player requirements
+ var roleSystem = _entManager.System();
+ var requirements = roleSystem.GetRolePlayerRequirements(antag);
+ if (requirements != null && !PassesRequirements(profile, requirements))
+ {
+ reason = SharedPlayerRequirementManager.GetCombinedReason(requirements);
+ return false;
+ }
+ // End DEN
+
return true;
}
// This must be private so code paths can't accidentally skip requirement overrides. Call this through IsAllowed()
+ [Obsolete("Use SharedPlayerRequirementManager.CheckRequirements() instead")] // DEN
private bool CheckRoleRequirements(HashSet? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
diff --git a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
new file mode 100644
index 0000000000..3515f3211d
--- /dev/null
+++ b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
@@ -0,0 +1,30 @@
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared._DEN.Requirements.PlayerRequirements;
+using Content.Shared.Preferences;
+using Content.Shared.Roles;
+
+namespace Content.Client.Players.PlayTimeTracking;
+
+public sealed partial class JobRequirementsManager
+{
+ ///
+ /// Check if we pass a given list of requirements.
+ /// A profile may be optionally supplied to replace the one in the context.
+ ///
+ ///
+ ///
+ ///
+ private bool PassesRequirements(HumanoidCharacterProfile? profile,
+ List requirements)
+ {
+ var session = _playerManager.LocalSession;
+ var context = session != null
+ ? _requirements.GetPlayerContext(session)
+ : new();
+
+ if (profile != null)
+ context.Profile = profile;
+
+ return SharedPlayerRequirementManager.CheckRequirements(context, requirements);
+ }
+}
diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
index 7d5f177a23..be17e9a5f8 100644
--- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
+++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
@@ -7,6 +7,7 @@
using Content.Server.GameTicking.Events;
using Content.Server.Preferences.Managers;
using Content.Server.Station.Events;
+using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mobs;
@@ -27,7 +28,7 @@ namespace Content.Server.Players.PlayTimeTracking;
///
/// Connects to the simulation state. Reports trackers and such.
///
-public sealed class PlayTimeTrackingSystem : EntitySystem
+public sealed partial class PlayTimeTrackingSystem : EntitySystem // DEN: Make partial
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IAfkManager _afk = default!;
@@ -35,6 +36,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
+ [Dependency] private readonly IPlayerRequirementManager _requirements = default!; // DEN
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly PlayTimeTrackingManager _tracking = default!;
@@ -249,15 +251,25 @@ public bool IsAllowed(ICommonSession player, ProtoId job)
playTimes = new Dictionary();
}
+ // Begin DEN: This is a guard clause now.
var requirements = _roles.GetRoleRequirements(job);
- return JobRequirements.TryRequirementsMet(
+ if (!JobRequirements.TryRequirementsMet(
requirements,
playTimes,
out _,
EntityManager,
_prototypes,
(HumanoidCharacterProfile?)
- _preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
+ _preferencesManager.GetPreferences(player.UserId).SelectedCharacter))
+ return false;
+ // End DEN
+ // Begin DEN: Use PlayerRequirements
+ var playerReqs = _roles.GetRolePlayerRequirements(job);
+ if (playerReqs != null && !PassesRequirements(player, playerReqs))
+ return false;
+
+ return true;
+ // End DEN
}
///
@@ -277,15 +289,25 @@ public bool IsAllowed(ICommonSession player, ProtoId antag)
playTimes = new Dictionary();
}
+ // Begin DEN: This is a guard clause now.
var requirements = _roles.GetRoleRequirements(antag);
- return JobRequirements.TryRequirementsMet(
+ if (!JobRequirements.TryRequirementsMet(
requirements,
playTimes,
out _,
EntityManager,
_prototypes,
(HumanoidCharacterProfile?)
- _preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
+ _preferencesManager.GetPreferences(player.UserId).SelectedCharacter))
+ return false;
+ // End DEN
+ // Begin DEN: Use PlayerRequirements
+ var playerReqs = _roles.GetRolePlayerRequirements(antag);
+ if (playerReqs != null && !PassesRequirements(player, playerReqs))
+ return false;
+
+ return true;
+ // End DEN
}
public HashSet> GetDisallowedJobs(ICommonSession player)
@@ -294,18 +316,21 @@ public HashSet> GetDisallowedJobs(ICommonSession player)
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
return roles;
- if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
- {
- Log.Error($"Unable to check playtimes {Environment.StackTrace}");
- playTimes = new Dictionary();
- }
+ // DEN: Commented out because we're using contexts now
+ // if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
+ // {
+ // Log.Error($"Unable to check playtimes {Environment.StackTrace}");
+ // playTimes = new Dictionary();
+ // }
+ // Begin DEN: Use PlayerRequirements
+ var context = _requirements.GetPlayerContext(player);
foreach (var job in _prototypes.EnumeratePrototypes())
{
- if (JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter))
+ if (IsJobAllowed(player, job, context))
roles.Add(job.ID);
}
-
+ // End DEN
return roles;
}
@@ -315,20 +340,21 @@ public void RemoveDisallowedJobs(NetUserId userId, List> j
return;
var player = _playerManager.GetSessionById(userId);
- if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
- {
- // Sorry mate but your playtimes haven't loaded.
- Log.Error($"Playtimes weren't ready yet for {player} on roundstart!");
- playTimes ??= new Dictionary();
- }
-
+ // DEN: Commented out because we're using contexts now
+ // if (!_tracking.TryGetTrackerTimes(player, out var playTimes))
+ // {
+ // // Sorry mate but your playtimes haven't loaded.
+ // Log.Error($"Playtimes weren't ready yet for {player} on roundstart!");
+ // playTimes ??= new Dictionary();
+ // }
+
+ // Begin DEN: Use PlayerRequirements
+ var context = _requirements.GetPlayerContext(player);
for (var i = 0; i < jobs.Count; i++)
{
- if (_prototypes.Resolve(jobs[i], out var job)
- && JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(userId).SelectedCharacter))
- {
+ if (_prototypes.Resolve(jobs[i], out var job) && IsJobAllowed(player, job, context))
continue;
- }
+ // End DEN
jobs.RemoveSwap(i);
i--;
diff --git a/Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
new file mode 100644
index 0000000000..8cdc7c57e4
--- /dev/null
+++ b/Content.Server/_DEN/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
@@ -0,0 +1,40 @@
+using Content.Shared._DEN.Requirements.Managers;
+using Content.Shared._DEN.Requirements.PlayerRequirements;
+using Content.Shared.Preferences;
+using Content.Shared.Roles;
+using Robust.Shared.Player;
+
+namespace Content.Server.Players.PlayTimeTracking;
+
+public sealed partial class PlayTimeTrackingSystem
+{
+ private bool PassesRequirements(ICommonSession player,
+ List requirements,
+ PlayerRequirementContext? context = null)
+ {
+ context ??= _requirements.GetPlayerContext(player);
+ return SharedPlayerRequirementManager.CheckRequirements(context, requirements);
+ }
+
+ private bool IsJobAllowed(ICommonSession player,
+ JobPrototype job,
+ PlayerRequirementContext? context = null)
+ {
+ context ??= _requirements.GetPlayerContext(player);
+ var playTimes = context.Playtimes ?? new Dictionary();
+ var profile = context.Profile
+ ?? (HumanoidCharacterProfile?)_preferencesManager.GetPreferences(player.UserId).SelectedCharacter;
+
+ // TODO DEN: This is deprecated.
+ var oldRequirementsMet = JobRequirements.TryRequirementsMet(job,
+ playTimes,
+ out _,
+ EntityManager,
+ _prototypes,
+ profile);
+
+ var playerReqs = _roles.GetRolePlayerRequirements(job);
+ var requirementsMet = playerReqs == null || PassesRequirements(player, playerReqs, context);
+ return oldRequirementsMet && requirementsMet;
+ }
+}
diff --git a/Content.Shared/Roles/AntagPrototype.cs b/Content.Shared/Roles/AntagPrototype.cs
index 367b05c3dd..3d12856e8a 100644
--- a/Content.Shared/Roles/AntagPrototype.cs
+++ b/Content.Shared/Roles/AntagPrototype.cs
@@ -1,3 +1,4 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
using Content.Shared.Guidebook;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
@@ -48,8 +49,17 @@ public sealed partial class AntagPrototype : IPrototype
/// Requirements that must be met to opt in to this antag role.
///
[DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)]
+ [Obsolete("Use PlayerRequirements instead")] // DEN
public HashSet? Requirements;
+ // Begin DEN: Use PlayerRequirements
+ ///
+ /// A list of PlayerRequirements for this role.
+ ///
+ [DataField("playerRequirements")]
+ public List? PlayerRequirements;
+ // End DEN
+
///
/// Optional list of guides associated with this antag. If the guides are opened, the first entry in this list
/// will be used to select the currently selected guidebook.
diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs
index 6b58c42428..2883b9b395 100644
--- a/Content.Shared/Roles/JobPrototype.cs
+++ b/Content.Shared/Roles/JobPrototype.cs
@@ -1,3 +1,4 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
using Content.Shared.Access;
using Content.Shared.Guidebook;
using Content.Shared.Players.PlayTimeTracking;
@@ -48,8 +49,17 @@ public sealed partial class JobPrototype : IPrototype
/// Requirements for the job.
///
[DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)]
+ [Obsolete("Use PlayerRequirements instead")] // DEN
public HashSet? Requirements;
+ // Begin DEN: Use PlayerRequirements
+ ///
+ /// A list of PlayerRequirements for this job.
+ ///
+ [DataField("playerRequirements")]
+ public List? PlayerRequirements;
+ // End DEN
+
///
/// When true - the station will have anouncement about arrival of this player.
///
diff --git a/Content.Shared/Roles/JobRequirementOverridePrototype.cs b/Content.Shared/Roles/JobRequirementOverridePrototype.cs
index d0ce649f36..9c43a63d64 100644
--- a/Content.Shared/Roles/JobRequirementOverridePrototype.cs
+++ b/Content.Shared/Roles/JobRequirementOverridePrototype.cs
@@ -1,3 +1,4 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
using Robust.Shared.Prototypes;
namespace Content.Shared.Roles;
@@ -13,8 +14,26 @@ public sealed partial class JobRequirementOverridePrototype : IPrototype
public string ID { get; private set; } = default!;
[DataField]
+ [Obsolete("Use JobRequirements instead")] // DEN
public Dictionary, HashSet> Jobs = new ();
[DataField]
+ [Obsolete("Use AntagRequirements instead")] // DEN
public Dictionary, HashSet> Antags = new ();
+
+ // Begin DEN: Use PlayerRequirements
+
+ ///
+ /// A dictionary of job roles mapped to a list of overriding requirements.
+ ///
+ [DataField]
+ public Dictionary, List> JobRequirements = new();
+
+ ///
+ /// A dictionary of antagonist roles mapped to a list of overriding requirements.
+ ///
+ [DataField]
+ public Dictionary, List> AntagRequirements = new();
+
+ // End DEN
}
diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs
index 47db09ebdb..29046f1e42 100644
--- a/Content.Shared/Roles/SharedRoleSystem.cs
+++ b/Content.Shared/Roles/SharedRoleSystem.cs
@@ -17,7 +17,7 @@
namespace Content.Shared.Roles;
-public abstract class SharedRoleSystem : EntitySystem
+public abstract partial class SharedRoleSystem : EntitySystem // DEN: Make partial
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -672,6 +672,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent
///
/// Returns the list of requirements for a role, or null. May be altered by requirement overrides.
///
+ [Obsolete("Use GetRolePlayerRequirements instead")] // DEN
public HashSet? GetRoleRequirements(JobPrototype job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
@@ -682,6 +683,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent
// TODO ROLES Change to readonly?
///
+ [Obsolete("Use GetRolePlayerRequirements instead")] // DEN
public HashSet? GetRoleRequirements(AntagPrototype antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))
@@ -692,6 +694,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent
// TODO ROLES Change to readonly?
///
+ [Obsolete("Use GetRolePlayerRequirements instead")] // DEN
public HashSet? GetRoleRequirements(ProtoId jobId)
{
return _prototypes.TryIndex(jobId, out var job) ? GetRoleRequirements(job) : null;
@@ -699,6 +702,7 @@ public void MindPlaySound(EntityUid mindId, SoundSpecifier? sound, MindComponent
// TODO ROLES Change to readonly?
///
+ [Obsolete("Use GetRolePlayerRequirements instead")] // DEN
public HashSet? GetRoleRequirements(ProtoId antagId)
{
return _prototypes.TryIndex(antagId, out var antag) ? GetRoleRequirements(antag) : null;
diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
index adbfef012f..6fda943220 100644
--- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
@@ -1,6 +1,8 @@
+using System.Text;
using Content.Shared._DEN.Requirements.PlayerRequirements;
using JetBrains.Annotations;
using Robust.Shared.Player;
+using Robust.Shared.Utility;
namespace Content.Shared._DEN.Requirements.Managers;
@@ -66,6 +68,43 @@ public static bool CheckRequirement(PlayerRequirementContext context, IPlayerReq
return true;
}
+ ///
+ /// Get a combined reason message for an enumerable collection of requirements.
+ ///
+ /// A collection of requirements.
+ /// A formatted message containing all the requirement reasons.
+ [PublicAPI]
+ public static FormattedMessage GetCombinedReason(IEnumerable requirements)
+ {
+ var messageBuilder = new StringBuilder();
+ foreach (var req in requirements)
+ messageBuilder.AppendLine(req.GetReason());
+
+ var messageString = messageBuilder.ToString().Trim();
+ return FormattedMessage.FromMarkupPermissive(messageString);
+ }
+
+ ///
+ /// Get a combined reason message for an enumerable collection of requirements.
+ /// This override will only include requirements that fail, given a context.
+ ///
+ /// A context to check against the requirements.
+ /// A collection of requirements.
+ /// A formatted message containing all the requirement reasons.
+ [PublicAPI]
+ public static FormattedMessage GetCombinedReason(PlayerRequirementContext context, IEnumerable requirements)
+ {
+ var messageBuilder = new StringBuilder();
+ foreach (var req in requirements)
+ {
+ if (!CheckRequirement(context, req))
+ messageBuilder.AppendLine(req.GetReason());
+ }
+
+ var messageString = messageBuilder.ToString().Trim();
+ return FormattedMessage.FromMarkupPermissive(messageString);
+ }
+
///
public abstract PlayerRequirementContext GetPlayerContext(ICommonSession session);
}
diff --git a/Content.Shared/_DEN/Roles/SharedRoleSystem.cs b/Content.Shared/_DEN/Roles/SharedRoleSystem.cs
new file mode 100644
index 0000000000..2f1cee4f74
--- /dev/null
+++ b/Content.Shared/_DEN/Roles/SharedRoleSystem.cs
@@ -0,0 +1,80 @@
+using Content.Shared._DEN.Requirements.PlayerRequirements;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Roles;
+
+public abstract partial class SharedRoleSystem
+{
+ ///
+ /// Get a list of requirements associated with a given job.
+ ///
+ ///
+ /// If a role requirement override is set for this job, the requirements associated
+ /// with the job in the override will be provided instead.
+ ///
+ /// The job to get player requirements for.
+ /// The job's actual requirements, accounting for the requirement CVar.
+ [PublicAPI]
+ public List? GetRolePlayerRequirements(JobPrototype job)
+ {
+ if (_requirementOverride != null
+ && _requirementOverride.JobRequirements.TryGetValue(job.ID, out var overrides))
+ return overrides;
+
+ return job.PlayerRequirements;
+ }
+
+ ///
+ /// Get a list of requirements associated with a given antagonist role.
+ ///
+ ///
+ /// If a role requirement override is set for this role, the requirements associated
+ /// with the role in the override will be provided instead.
+ ///
+ /// The antagonist role to get player requirements for.
+ /// The role's actual requirements, accounting for the requirement CVar.
+ [PublicAPI]
+ public List? GetRolePlayerRequirements(AntagPrototype antag)
+ {
+ if (_requirementOverride != null
+ && _requirementOverride.AntagRequirements.TryGetValue(antag.ID, out var overrides))
+ return overrides;
+
+ return antag.PlayerRequirements;
+ }
+
+ ///
+ /// Get a list of requirements associated with a given job.
+ ///
+ ///
+ /// If a role requirement override is set for this job, the requirements associated
+ /// with the job in the override will be provided instead.
+ ///
+ /// The ID of a job to get player requirements for.
+ /// The job's actual requirements, accounting for the requirement CVar.
+ [PublicAPI]
+ public List? GetRolePlayerRequirements(ProtoId jobId)
+ {
+ return _prototypes.TryIndex(jobId, out var job)
+ ? GetRolePlayerRequirements(job)
+ : null;
+ }
+
+ ///
+ /// Get a list of requirements associated with a given antagonist role.
+ ///
+ ///
+ /// If a role requirement override is set for this role, the requirements associated
+ /// with the role in the override will be provided instead.
+ ///
+ /// The Id of an antagonist role to get player requirements for.
+ /// The role's actual requirements, accounting for the requirement CVar.
+ [PublicAPI]
+ public List? GetRolePlayerRequirements(ProtoId antagId)
+ {
+ return _prototypes.TryIndex(antagId, out var antag)
+ ? GetRolePlayerRequirements(antag)
+ : null;
+ }
+}
From b0a3c6ee045a2965e3b8a24a09d8776e386f262d Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 18:38:01 -0500
Subject: [PATCH 40/54] add debug traits for requirements
---
Resources/Locale/en-US/_DEN/traits/debug.ftl | 2 +
.../Prototypes/_DEN/EntityTraits/debug.yml | 38 +++++++++++++++++++
2 files changed, 40 insertions(+)
diff --git a/Resources/Locale/en-US/_DEN/traits/debug.ftl b/Resources/Locale/en-US/_DEN/traits/debug.ftl
index 00267de3ee..b9cb00f369 100644
--- a/Resources/Locale/en-US/_DEN/traits/debug.ftl
+++ b/Resources/Locale/en-US/_DEN/traits/debug.ftl
@@ -1,3 +1,5 @@
trait-debug-jittery-name = Jittery
trait-debug-stunned-name = Stunned
trait-debug-spawn-crowbars-name = Spawn Crowbars
+trait-debug-requirements-playtime-name = Playtime Requirements
+trait-debug-requirements-profile-name = Profile Requirements
diff --git a/Resources/Prototypes/_DEN/EntityTraits/debug.yml b/Resources/Prototypes/_DEN/EntityTraits/debug.yml
index b2435e9035..1cb2697f61 100644
--- a/Resources/Prototypes/_DEN/EntityTraits/debug.yml
+++ b/Resources/Prototypes/_DEN/EntityTraits/debug.yml
@@ -32,3 +32,41 @@
- Crowbar
- Crowbar
- Crowbar
+
+- type: entityTrait
+ id: DebugRequirementsPlaytime
+ name: trait-debug-requirements-playtime-name
+ characterEditorSelectable: false
+ functions: []
+ requirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Command
+ minTime: 86400
+ - !type:PlayerJobPlaytimeRequirement
+ job: Clown
+ maxTime: 8340
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 3600
+ maxTime: 14400
+
+- type: entityTrait
+ id: DebugRequirementsProfile
+ name: trait-debug-requirements-profile-name
+ characterEditorSelectable: false
+ functions: []
+ requirements:
+ - !type:PlayerAgeRequirement
+ max: 60
+ - !type:PlayerSpeciesRequirement
+ species:
+ - Human
+ - Vulpkanin
+ - SlimePerson
+ - !type:PlayerTraitRequirement
+ inverted: true
+ traits:
+ - PoorVision
+ - Blindness
+ count: !type:RangeCountRequirement
+ min: 2
+ max: 8
From dc7b9299190549bb61333cb2d5b9af2b659639e7 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 19:12:13 -0500
Subject: [PATCH 41/54] add comments to these fields
---
.../_DEN/Players/PlayTimeTracking/JobRequirementManager.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
index 3515f3211d..9de5ae08af 100644
--- a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
+++ b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
@@ -11,9 +11,9 @@ public sealed partial class JobRequirementsManager
/// Check if we pass a given list of requirements.
/// A profile may be optionally supplied to replace the one in the context.
///
- ///
- ///
- ///
+ /// A profile associated with the character we're loading in.
+ /// The requirements to check.
+ /// Whether we pass the requirements given.
private bool PassesRequirements(HumanoidCharacterProfile? profile,
List requirements)
{
From 0fcd30e26c5a9af2ab943c19870adc383ed5d263 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Thu, 26 Mar 2026 19:18:08 -0500
Subject: [PATCH 42/54] remove "shouldHide" traits from characters
---
Content.Shared/Preferences/HumanoidCharacterProfile.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index 86252c1a53..6017de228d 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -616,7 +616,9 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
context.Profile = this;
var traits = EntityTraitPreferences
- .Where(t => prototypeManager.TryIndex(t, out var trait) && trait.Selectable)
+ .Where(t => prototypeManager.TryIndex(t, out var trait)
+ && trait.Selectable
+ && !SharedPlayerRequirementManager.ShouldHide(context, trait.Requirements))
.ToList();
// End DEN
From de4b86e97ed26c481f0930c34c836a0a4f1abcd9 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:35:18 -0500
Subject: [PATCH 43/54] include context in player requirement reasons
---
.../UI/Traits/EntityTraitSelector.xaml.cs | 29 ++++++++++++++-----
.../Effects/PlayerRequirementLoadoutEffect.cs | 2 +-
.../SharedPlayerRequirementManager.cs | 11 +++----
.../PlayerRequirements/CountRequirement.cs | 12 ++++----
.../PlayerRequirements/IPlayerRequirement.cs | 5 ++--
.../PlayerRequirements.Playtime.cs | 6 ++--
.../PlayerRequirements.Profile.cs | 8 ++---
7 files changed, 46 insertions(+), 27 deletions(-)
diff --git a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
index ba49b0bb79..ea6e832c62 100644
--- a/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
+++ b/Content.Client/_DEN/Lobby/UI/Traits/EntityTraitSelector.xaml.cs
@@ -57,16 +57,26 @@ private void SetTrait(EntityTraitPrototype trait)
}
///
- /// Toggles the visibility of this selector, depending on the requirements of a given trait.
+ /// Retrieve this session's context.
///
- /// The trait to check the requirements of.
- private void UpdateVisibility(EntityTraitPrototype trait)
+ /// The local player's session context.
+ private PlayerRequirementContext GetContext()
{
var session = _player.LocalSession;
if (session == null)
- return;
+ return new();
var context = _requirements.GetPlayerContext(session);
+ return context;
+ }
+
+ ///
+ /// Toggles the visibility of this selector, depending on the requirements of a given trait.
+ ///
+ /// The trait to check the requirements of.
+ private void UpdateVisibility(EntityTraitPrototype trait)
+ {
+ var context = GetContext();
Visible = !SharedPlayerRequirementManager.ShouldHide(context, trait.Requirements);
}
@@ -80,7 +90,11 @@ private void UpdateVisibility(EntityTraitPrototype trait)
if (_trait is null)
return null;
- var tooltipString = ConstructTooltipDescription(_trait);
+ var context = _trait.Requirements.Count > 0
+ ? GetContext()
+ : null;
+
+ var tooltipString = ConstructTooltipDescription(_trait, context);
if (tooltipString.Length == 0)
return null;
@@ -96,7 +110,8 @@ private void UpdateVisibility(EntityTraitPrototype trait)
///
/// The trait to construct a description out of.
/// The tooltip text associated with this trait.
- private static string ConstructTooltipDescription(EntityTraitPrototype trait)
+ private static string ConstructTooltipDescription(EntityTraitPrototype trait,
+ PlayerRequirementContext? context = null)
{
var tooltipBuilder = new StringBuilder();
@@ -110,7 +125,7 @@ private static string ConstructTooltipDescription(EntityTraitPrototype trait)
tooltipBuilder.AppendLine(); // Empty line
foreach (var requirement in trait.Requirements)
{
- var reason = requirement.GetReason();
+ var reason = requirement.GetReason(context);
if (reason is null)
continue;
diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
index 269e323c67..e5d4e7b159 100644
--- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
+++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
@@ -38,7 +38,7 @@ public override bool Validate(HumanoidCharacterProfile profile,
if (!success)
{
- var reasonText = Requirement.GetReason() ?? string.Empty;
+ var reasonText = Requirement.GetReason(context) ?? string.Empty;
reason = FormattedMessage.FromMarkupPermissive(reasonText);
return false;
}
diff --git a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
index 6fda943220..26175b955d 100644
--- a/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
+++ b/Content.Shared/_DEN/Requirements/Managers/SharedPlayerRequirementManager.cs
@@ -72,13 +72,14 @@ public static bool CheckRequirement(PlayerRequirementContext context, IPlayerReq
/// Get a combined reason message for an enumerable collection of requirements.
///
/// A collection of requirements.
+ /// A context to check against the requirements.
/// A formatted message containing all the requirement reasons.
[PublicAPI]
- public static FormattedMessage GetCombinedReason(IEnumerable requirements)
+ public static FormattedMessage GetCombinedReason(IEnumerable requirements, PlayerRequirementContext? context = null)
{
var messageBuilder = new StringBuilder();
foreach (var req in requirements)
- messageBuilder.AppendLine(req.GetReason());
+ messageBuilder.AppendLine(req.GetReason(context));
var messageString = messageBuilder.ToString().Trim();
return FormattedMessage.FromMarkupPermissive(messageString);
@@ -86,19 +87,19 @@ public static FormattedMessage GetCombinedReason(IEnumerable
///
/// Get a combined reason message for an enumerable collection of requirements.
- /// This override will only include requirements that fail, given a context.
+ /// This function will only include requirements that fail, given a context.
///
/// A context to check against the requirements.
/// A collection of requirements.
/// A formatted message containing all the requirement reasons.
[PublicAPI]
- public static FormattedMessage GetCombinedReason(PlayerRequirementContext context, IEnumerable requirements)
+ public static FormattedMessage GetFailedCombinedReason(PlayerRequirementContext context, IEnumerable requirements)
{
var messageBuilder = new StringBuilder();
foreach (var req in requirements)
{
if (!CheckRequirement(context, req))
- messageBuilder.AppendLine(req.GetReason());
+ messageBuilder.AppendLine(req.GetReason(context));
}
var messageString = messageBuilder.ToString().Trim();
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
index 0f0a105f17..5021c52255 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Shared._DEN.Requirements.Managers;
using JetBrains.Annotations;
namespace Content.Shared._DEN.Requirements.PlayerRequirements;
@@ -20,8 +21,9 @@ public abstract partial class CountRequirement
/// This would slot into the sentence "Must have [reason] of the following items: [items]".
///
/// "At least 1", "between 2 and 5", "all"
+ /// An optional context used to add additional information to the reason.
/// A string representation of this range's requirement bounds.
- public abstract string GetReason();
+ public abstract string GetReason(PlayerRequirementContext? context = null);
///
/// Check if our currently-selected items in a collection meets this
@@ -58,7 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement
public int Count;
///
- public override string GetReason()
+ public override string GetReason(PlayerRequirementContext? context = null)
{
// "Must have exactly 1 of the following items."
return Loc.GetString("count-requirement-constant-reason",
@@ -91,7 +93,7 @@ public sealed partial class RangeCountRequirement : CountRequirement, IPlayerRan
public int? Max { get; set; } = null;
///
- public override string GetReason()
+ public override string GetReason(PlayerRequirementContext? context = null)
{
if (this is IPlayerRangeRequirement range)
return range.GetRangeConstraintReason();
@@ -133,7 +135,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
public sealed partial class AnyCountRequirement : CountRequirement
{
///
- public override string GetReason()
+ public override string GetReason(PlayerRequirementContext? context = null)
{
// "Must have any of the following items."
return Loc.GetString("count-requirement-any-reason");
@@ -153,7 +155,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
public sealed partial class AllCountRequirement : CountRequirement
{
///
- public override string GetReason()
+ public override string GetReason(PlayerRequirementContext? context = null)
{
// "Must have all of the following items."
return Loc.GetString("count-requirement-all-reason");
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index a1cc4bb623..693cfa68a9 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -66,8 +66,9 @@ public partial interface IPlayerRequirement
///
/// This should display differently depending on the value of .
///
+ /// An optional context used to add additional information to the reason.
/// An optional string representing the requirement text to display to a player.
- string? GetReason();
+ string? GetReason(PlayerRequirementContext? context = null);
}
///
@@ -88,7 +89,7 @@ public abstract partial class PlayerRequirement : IPlayerRequirement
public abstract bool CheckRequirement(PlayerRequirementContext context);
///
- public abstract string? GetReason();
+ public abstract string? GetReason(PlayerRequirementContext? context = null);
///
public abstract bool PreCheck(PlayerRequirementContext context);
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 23baf5403a..2615819143 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -154,7 +154,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason()
+ public override string? GetReason(PlayerRequirementContext? context = null)
{
// Do not give a reason if role timers are disabled.
if (ShouldAutoPass())
@@ -253,7 +253,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason()
+ public override string? GetReason(PlayerRequirementContext? context = null)
{
// Do not give a reason if role timers are disabled.
if (ShouldAutoPass())
@@ -362,7 +362,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason()
+ public override string? GetReason(PlayerRequirementContext? context = null)
{
// Do not give a reason if role timers are disabled.
if (ShouldAutoPass())
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index f0aad45a13..09df8b3e39 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -43,7 +43,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason()
+ public override string? GetReason(PlayerRequirementContext? context = null)
{
if (!TryGetRangeConstraintReason(out var constraintReason))
return null;
@@ -109,7 +109,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason()
+ public override string? GetReason(PlayerRequirementContext? context = null)
{
var protoMan = IoCManager.Resolve();
@@ -174,12 +174,12 @@ public override bool CheckRequirement(PlayerRequirementContext context)
}
///
- public override string? GetReason()
+ public override string? GetReason(PlayerRequirementContext? context = null)
{
var protoMan = IoCManager.Resolve();
var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan));
var traitList = string.Join(", ", traitNames);
- var constraintReason = Count.GetReason();
+ var constraintReason = Count.GetReason(context);
return Loc.GetString("player-requirement-trait-reason",
("inverted", Inverted),
From e2775dad519680b7e1c5aae999f41438f88b6a36 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 13:44:04 -0500
Subject: [PATCH 44/54] add difference ranges to player range requirements
---
.../PlayerRequirements/CountRequirement.cs | 33 ++-
.../PlayerRequirements/IPlayerRequirement.cs | 88 ++++++-
.../PlayerRequirements.Playtime.cs | 249 +++++++++++-------
.../PlayerRequirements.Profile.cs | 69 ++++-
.../requirements/playtime-requirements.ftl | 2 +-
.../_DEN/requirements/requirement-range.ftl | 5 +
6 files changed, 324 insertions(+), 122 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
index 5021c52255..ff3c2b8862 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/CountRequirement.cs
@@ -23,7 +23,7 @@ public abstract partial class CountRequirement
/// "At least 1", "between 2 and 5", "all"
/// An optional context used to add additional information to the reason.
/// A string representation of this range's requirement bounds.
- public abstract string GetReason(PlayerRequirementContext? context = null);
+ public abstract string GetReason();
///
/// Check if our currently-selected items in a collection meets this
@@ -60,7 +60,7 @@ public sealed partial class ConstantCountRequirement : CountRequirement
public int Count;
///
- public override string GetReason(PlayerRequirementContext? context = null)
+ public override string GetReason()
{
// "Must have exactly 1 of the following items."
return Loc.GetString("count-requirement-constant-reason",
@@ -93,7 +93,7 @@ public sealed partial class RangeCountRequirement : CountRequirement, IPlayerRan
public int? Max { get; set; } = null;
///
- public override string GetReason(PlayerRequirementContext? context = null)
+ public override string GetReason()
{
if (this is IPlayerRangeRequirement range)
return range.GetRangeConstraintReason();
@@ -113,15 +113,30 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
}
///
- public string? GetMinText()
+ public string FormatValue(int value)
{
- return Min?.ToString();
+ return value.ToString();
}
///
- public string? GetMaxText()
+ public int? GetDifference(int value)
{
- return Max?.ToString();
+ // Unimplemented - unused
+ throw new NotImplementedException();
+ }
+
+ ///
+ public int Sign(int difference)
+ {
+ // Unimplemented - unused
+ throw new NotImplementedException();
+ }
+
+ ///
+ public string FormatDifferenceText(int difference)
+ {
+ // Unimplemented - unused
+ throw new NotImplementedException();
}
}
@@ -135,7 +150,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
public sealed partial class AnyCountRequirement : CountRequirement
{
///
- public override string GetReason(PlayerRequirementContext? context = null)
+ public override string GetReason()
{
// "Must have any of the following items."
return Loc.GetString("count-requirement-any-reason");
@@ -155,7 +170,7 @@ public override bool CheckRequirement(IEnumerable have, IEnumerable req
public sealed partial class AllCountRequirement : CountRequirement
{
///
- public override string GetReason(PlayerRequirementContext? context = null)
+ public override string GetReason()
{
// "Must have all of the following items."
return Loc.GetString("count-requirement-all-reason");
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
index 693cfa68a9..01b83c44c2 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/IPlayerRequirement.cs
@@ -133,7 +133,7 @@ bool IsInRange(T value)
///
/// Get a reason message to display to the player for this requirement's allowed value range.
///
- string GetRangeConstraintReason()
+ string GetRangeConstraintReason(PlayerRequirementContext? context = null)
{
var minText = GetMinText();
var maxText = GetMaxText();
@@ -157,13 +157,95 @@ string GetRangeConstraintReason()
};
}
+ ///
+ /// Get the difference between this value and the maximum or minimum value of this range.
+ /// Should return null if the value is in range.
+ ///
+ /// The value to get a difference of.
+ /// The difference between the value and the nearest bound of this range.
+ T? GetDifference(T value);
+
+ ///
+ /// Get the "sign" associated with this difference.
+ ///
+ ///
+ /// "1" means the difference is lower than the minimum.
+ /// "-1" means the difference is higher than the maximum.
+ /// "0" means the difference is within range.
+ ///
+ /// The difference value.
+ /// The sign of this difference.
+ int Sign(T difference);
+
+ ///
+ /// Format this difference value as a string to display to the player.
+ ///
+ /// The difference value.
+ /// A string representation of this difference.
+ string FormatDifferenceText(T difference);
+
+ ///
+ /// Format a "difference" value depending on if it is higher than the maximum or lower than the minimum.
+ ///
+ /// The difference value to format.
+ /// A string representation of this difference.
+ string FormatDifference(T difference)
+ {
+ var diffText = FormatDifferenceText(difference);
+ return Sign(difference) switch
+ {
+ 1 => Loc.GetString("player-requirement-range-difference-lower", ("count", diffText)),
+ -1 => Loc.GetString("player-requirement-range-difference-higher", ("count", diffText)),
+ _ => "0",
+ };
+ }
+
+ ///
+ /// Get a string representation between this value and its distance from the nearest bound.
+ /// Returns null if the value is in range.
+ ///
+ /// A value being tested against the requirement.
+ /// A string representation of the difference between the value and the requirement's bounds.
+ string? GetDifferenceReason(T value)
+ {
+ var difference = GetDifference(value);
+ if (difference == null)
+ return null;
+
+ var diffValue = FormatDifference(difference.Value) ?? difference.Value.ToString();
+ if (diffValue == null)
+ return null;
+
+ return Loc.GetString("player-requirement-range-difference",
+ ("difference", diffValue));
+ }
+
+ ///
+ /// Format a value of the range type into a string.
+ ///
+ /// The value to format.
+ /// A string representation of this value.
+ string FormatValue(T value);
+
///
/// Get a string representation of this range's minimum value.
///
- string? GetMinText();
+ string? GetMinText()
+ {
+ if (Min == null)
+ return null;
+
+ return FormatValue(Min.Value);
+ }
///
/// Get a string representation of this range's maximum value.
///
- string? GetMaxText();
+ string? GetMaxText()
+ {
+ if (Max == null)
+ return null;
+
+ return FormatValue(Max.Value);
+ }
}
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 2615819143..1e4fa1c07e 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -44,6 +44,42 @@ public override bool PreCheck(PlayerRequirementContext context)
return ShouldAutoPass() || context.Playtimes != null;
}
+ ///
+ public override bool CheckRequirement(PlayerRequirementContext context)
+ {
+ // Auto-pass if role timers are disabled.
+ if (ShouldAutoPass())
+ return true;
+
+ var playtime = GetPlaytime(context);
+ if (playtime is null)
+ return false;
+
+ return IsInRange(playtime.Value);
+ }
+
+ ///
+ public override string? GetReason(PlayerRequirementContext? context = null)
+ {
+ // Do not give a reason if role timers are disabled.
+ if (ShouldAutoPass())
+ return null;
+
+ // Get the playtime constraint string.
+ if (!TryGetRangeConstraintReason(out var constraintReason))
+ return null;
+
+ // If there's no "difference reason", then just the constraint is fine.
+ // "Must have 2h of playtime overall."
+ if (!TryGetRangeDifferenceReason(out var differenceReason, context))
+ return constraintReason;
+
+ // "Must have 2h of playtime overall. (Need 1h more)"
+ return Loc.GetString("player-requirement-range-with-difference",
+ ("range", constraintReason),
+ ("difference", differenceReason));
+ }
+
///
/// Whether or not this requirement should auto-pass. This applies if role timers
/// are disabled, because playtimes shouldn't matter anyway in this case - we shouldn't
@@ -70,28 +106,13 @@ public bool ShouldAutoPass()
///
/// The playtime to format.
/// The formatted playtime, if playtime is not null.
- private static string? FormatPlaytime(TimeSpan? playtime)
+ private static string FormatPlaytime(TimeSpan playtime)
{
- if (playtime is null)
- return null;
-
- var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime.Value);
+ var playtimeString = ContentLocalizationManager.FormatPlaytime(playtime);
return Loc.GetString("player-requirement-format-time",
("playtime", playtimeString));
}
- ///
- public string? GetMinText()
- {
- return FormatPlaytime(Min);
- }
-
- ///
- public string? GetMaxText()
- {
- return FormatPlaytime(Max);
- }
-
///
/// Check if the given playtime is in range.
///
@@ -105,20 +126,93 @@ protected bool IsInRange(TimeSpan playtime)
return false;
}
+ ///
+ public string FormatValue(TimeSpan value)
+ {
+ return FormatPlaytime(value);
+ }
+
+ ///
+ public TimeSpan? GetDifference(TimeSpan value)
+ {
+ if (this is IPlayerRangeRequirement range && range.IsInRange(value))
+ return null;
+
+ // Negative value = greater than maximum
+ if (Max != null && value > Max)
+ return Max - value;
+
+ // Positive value = less than minimum
+ if (Min != null && value < Min)
+ return Min - value;
+
+ return null;
+ }
+
+ ///
+ public int Sign(TimeSpan difference)
+ {
+ return Math.Sign(difference.TotalSeconds);
+ }
+
+ ///
+ public string FormatDifferenceText(TimeSpan difference)
+ {
+ var diffValue = difference.Duration();
+ return FormatValue(diffValue);
+ }
+
///
/// Get the text to display to the player that represents the range of valid playtimes.
///
/// The playtime range description.
/// Whether or not this operation was successful.
- protected bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString)
+ protected virtual bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString)
{
playtimeString = null;
if (this is IPlayerRangeRequirement range)
- playtimeString = range.GetRangeConstraintReason();
+ {
+ var constraintReason = range.GetRangeConstraintReason();
+ playtimeString = Loc.GetString("player-requirement-playtime-constraint-reason",
+ ("inverted", Inverted),
+ ("constraint", constraintReason));
+ }
return playtimeString != null;
}
+
+ ///
+ /// Get the text to display to the player that represents the difference between their
+ /// playtime and the upper/lower bound of acceptable playtimes.
+ ///
+ /// A string representing the player's playtime in relation to the requirement's bounds.
+ /// A context that may contain this character's playtimes.
+ /// Whether or not this operation was successful.
+ protected virtual bool TryGetRangeDifferenceReason([NotNullWhen(true)] out string? reason,
+ PlayerRequirementContext? context = null)
+ {
+ reason = null;
+
+ if (context?.Profile == null)
+ return false;
+
+ if (this is IPlayerRangeRequirement range)
+ {
+ var playtime = GetPlaytime(context);
+ if (playtime != null)
+ reason = range.GetDifferenceReason(playtime.Value);
+ }
+
+ return reason != null;
+ }
+
+ ///
+ /// Get the playtime associated with this requirement.
+ ///
+ /// The context that may or may not contain playtimes.
+ /// The playtime associated with this requirement.
+ protected abstract TimeSpan? GetPlaytime(PlayerRequirementContext context);
}
[Serializable]
@@ -139,43 +233,27 @@ public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytime
[DataField(required: true)]
public ProtoId Department = default!;
- ///
- public override bool CheckRequirement(PlayerRequirementContext context)
+ ///
+ protected override TimeSpan? GetPlaytime(PlayerRequirementContext context)
{
- // Auto-pass if role timers are disabled.
- if (ShouldAutoPass())
- return true;
-
- var playtime = GetDepartmentPlaytime(context);
- if (playtime is null)
- return false;
-
- return IsInRange(playtime.Value);
+ return GetDepartmentPlaytime(context);
}
- ///
- public override string? GetReason(PlayerRequirementContext? context = null)
+ ///
+ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString)
{
- // Do not give a reason if role timers are disabled.
- if (ShouldAutoPass())
- return null;
+ if (!base.TryGetRangeConstraintReason(out playtimeString))
+ return false;
- // Get a formatted department name.
var protoMan = IoCManager.Resolve();
var deptName = FormatDepartment(protoMan);
- // Get the playtime constraint string.
- if (!TryGetRangeConstraintReason(out var playtimeString))
- return null;
-
- var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
- ("inverted", Inverted),
- ("timeConstraint", playtimeString));
-
// E.g. "You must have 2h30m of playtime in the Science department."
- return Loc.GetString("player-requirement-department-playtime-reason",
- ("constraint", constraintReason),
+ playtimeString = Loc.GetString("player-requirement-department-playtime-reason",
+ ("constraint", playtimeString),
("department", deptName));
+
+ return true;
}
///
@@ -238,43 +316,27 @@ public sealed partial class PlayerJobPlaytimeRequirement : PlayerPlaytimeRequire
[DataField(required: true)]
public ProtoId Job = default!;
- ///
- public override bool CheckRequirement(PlayerRequirementContext context)
+ ///
+ protected override TimeSpan? GetPlaytime(PlayerRequirementContext context)
{
- // Auto-pass if role timers are disabled.
- if (ShouldAutoPass())
- return true;
-
- var playtime = GetJobPlaytime(context);
- if (playtime is null)
- return false;
-
- return IsInRange(playtime.Value);
+ return GetJobPlaytime(context);
}
- ///
- public override string? GetReason(PlayerRequirementContext? context = null)
+ ///
+ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString)
{
- // Do not give a reason if role timers are disabled.
- if (ShouldAutoPass())
- return null;
+ if (!base.TryGetRangeConstraintReason(out playtimeString))
+ return false;
- // Get the job name and format it with a color.
var protoMan = IoCManager.Resolve();
var jobName = FormatJob(protoMan);
- // Get the playtime constraint string.
- if (!TryGetRangeConstraintReason(out var playtimeString))
- return null;
-
- var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
- ("inverted", Inverted),
- ("timeConstraint", playtimeString));
-
// E.g. "You must have 2h30m of playtime as a Mime."
- return Loc.GetString("player-requirement-job-playtime-reason",
- ("constraint", constraintReason),
+ playtimeString = Loc.GetString("player-requirement-job-playtime-reason",
+ ("constraint", playtimeString),
("job", jobName));
+
+ return true;
}
///
@@ -328,18 +390,23 @@ private string FormatJob(IPrototypeManager protoMan)
///
public sealed partial class PlayerOverallPlaytimeRequirement : PlayerPlaytimeRequirement
{
- ///
- public override bool CheckRequirement(PlayerRequirementContext context)
+ ///
+ protected override TimeSpan? GetPlaytime(PlayerRequirementContext context)
{
- // Auto-pass if role timers are disabled.
- if (ShouldAutoPass())
- return true;
+ return GetOverallPlaytime(context);
+ }
- var playtime = GetOverallPlaytime(context);
- if (playtime is null)
+ ///
+ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? playtimeString)
+ {
+ if (!base.TryGetRangeConstraintReason(out playtimeString))
return false;
- return IsInRange(playtime.Value);
+ // E.g. "You must have 300h of playtime overall."
+ playtimeString = Loc.GetString("player-requirement-overall-playtime-reason",
+ ("constraint", playtimeString));
+
+ return true;
}
///
@@ -360,24 +427,4 @@ public override bool CheckRequirement(PlayerRequirementContext context)
return playtime;
}
-
- ///
- public override string? GetReason(PlayerRequirementContext? context = null)
- {
- // Do not give a reason if role timers are disabled.
- if (ShouldAutoPass())
- return null;
-
- // Get the playtime constraint string.
- if (!TryGetRangeConstraintReason(out var playtimeString))
- return null;
-
- var constraintReason = Loc.GetString("player-requirement-playtime-constraint-reason",
- ("inverted", Inverted),
- ("timeConstraint", playtimeString));
-
- // E.g. "You must have 300h of playtime overall."
- return Loc.GetString("player-requirement-overall-playtime-reason",
- ("constraint", constraintReason));
- }
}
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index 09df8b3e39..568c6ad790 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -49,15 +49,22 @@ public override bool CheckRequirement(PlayerRequirementContext context)
return null;
// "Must be between 20 and 40 years old."
- return Loc.GetString("player-requirement-age-reason",
+ var rangeReason = Loc.GetString("player-requirement-age-reason",
("inverted", Inverted),
("constraint", constraintReason));
+
+ if (!TryGetRangeDifferenceReason(out var differenceReason, context))
+ return rangeReason;
+
+ return Loc.GetString("player-requirement-range-with-difference",
+ ("range", rangeReason),
+ ("difference", differenceReason));
}
///
/// Get the text to display to the player that represents the range of valid playtimes.
///
- /// The playtime range description.
+ /// The age range description.
/// Whether or not this operation was successful.
private bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? reason)
{
@@ -69,16 +76,62 @@ private bool TryGetRangeConstraintReason([NotNullWhen(true)] out string? reason)
return reason != null;
}
- ///
- public string? GetMinText()
+ ///
+ /// Get the text to display to the player that represents the difference between their
+ /// character's age and the upper/lower bound of acceptable ages.
+ ///
+ /// A string representing this character's age in relation to the requirement's bounds.
+ /// A context that may contain this character's profile.
+ /// Whether or not this operation was successful.
+ private bool TryGetRangeDifferenceReason([NotNullWhen(true)] out string? reason,
+ PlayerRequirementContext? context = null)
+ {
+ reason = null;
+
+ if (context?.Profile == null)
+ return false;
+
+ var age = context.Profile.Age;
+ if (this is IPlayerRangeRequirement range)
+ reason = range.GetDifferenceReason(age);
+
+ return reason != null;
+ }
+
+ ///
+ public string FormatValue(int value)
{
- return Min?.ToString();
+ return value.ToString();
}
///
- public string? GetMaxText()
+ public int? GetDifference(int value)
+ {
+ if (this is IPlayerRangeRequirement range && range.IsInRange(value))
+ return null;
+
+ // Negative value = greater than maximum
+ if (Max != null && value > Max)
+ return Max - value;
+
+ // Positive value = less than minimum
+ if (Min != null && value < Min)
+ return Min - value;
+
+ return null;
+ }
+
+ ///
+ public int Sign(int difference)
+ {
+ return Math.Sign(difference);
+ }
+
+ ///
+ public string FormatDifferenceText(int difference)
{
- return Max?.ToString();
+ var diffValue = Math.Abs(difference);
+ return FormatValue(diffValue);
}
}
@@ -179,7 +232,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
var protoMan = IoCManager.Resolve();
var traitNames = Traits.Select(t => LocalizeTrait(t, protoMan));
var traitList = string.Join(", ", traitNames);
- var constraintReason = Count.GetReason(context);
+ var constraintReason = Count.GetReason();
return Loc.GetString("player-requirement-trait-reason",
("inverted", Inverted),
diff --git a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
index 64bbd8ba84..fa56fcf911 100644
--- a/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/playtime-requirements.ftl
@@ -4,7 +4,7 @@ player-requirement-playtime-minmax-time = between {$minimum} and {$maximum}
player-requirement-playtime-constraint-reason = Must{$inverted ->
[true] {" "}not
*[false] {""}
-} have {$timeConstraint} of playtime
+} have {$constraint} of playtime
player-requirement-department-playtime-reason = {$constraint} in the {$department} department.
player-requirement-job-playtime-reason = {$constraint} as a {$job}.
diff --git a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
index 2a88288b75..eee2764983 100644
--- a/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/requirement-range.ftl
@@ -2,6 +2,11 @@ player-requirement-range-minimum-reason = at least {$minimum}
player-requirement-range-maximum-reason = at most {$maximum}
player-requirement-range-minmax-reason = between {$minimum} and {$maximum}
+player-requirement-range-with-difference = {$range} {$difference}
+player-requirement-range-difference = (Need {$difference})
+player-requirement-range-difference-lower = {$count} more
+player-requirement-range-difference-higher = {$count} less
+
count-requirement-constant-reason = exactly {$count}
count-requirement-any-reason = any
count-requirement-all-reason = all
From 30891b0d995d3241aeee1bd30ed4aa3fb5c1e17a Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 14:10:38 -0500
Subject: [PATCH 45/54] give jobs/antags timer requirement difference reasons
---
.../Players/PlayTimeTracking/JobRequirementsManager.cs | 8 ++++----
.../Players/PlayTimeTracking/JobRequirementManager.cs | 6 ++++--
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index 94edcc3746..04ac5ddce7 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -158,9 +158,9 @@ public bool IsAllowed(
// Begin DEN: Use player requirements
var roleSystem = _entManager.System();
var requirements = roleSystem.GetRolePlayerRequirements(job);
- if (requirements != null && !PassesRequirements(profile, requirements))
+ if (requirements != null && !PassesRequirements(profile, requirements, out var context))
{
- reason = SharedPlayerRequirementManager.GetCombinedReason(requirements);
+ reason = SharedPlayerRequirementManager.GetCombinedReason(requirements, context);
return false;
}
// End DEN
@@ -196,9 +196,9 @@ public bool IsAllowed(
// Begin DEN: Use player requirements
var roleSystem = _entManager.System();
var requirements = roleSystem.GetRolePlayerRequirements(antag);
- if (requirements != null && !PassesRequirements(profile, requirements))
+ if (requirements != null && !PassesRequirements(profile, requirements, out var context))
{
- reason = SharedPlayerRequirementManager.GetCombinedReason(requirements);
+ reason = SharedPlayerRequirementManager.GetCombinedReason(requirements, context);
return false;
}
// End DEN
diff --git a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
index 9de5ae08af..143ac5d323 100644
--- a/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
+++ b/Content.Client/_DEN/Players/PlayTimeTracking/JobRequirementManager.cs
@@ -13,12 +13,14 @@ public sealed partial class JobRequirementsManager
///
/// A profile associated with the character we're loading in.
/// The requirements to check.
+ ///
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index b6cd8ed953..ba24121a2e 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -104,7 +104,8 @@ private bool TryGetRangeDifferenceReason([NotNullWhen(true)] out string? reason,
///
public string FormatValue(int value)
{
- return value.ToString();
+ return Loc.GetString("player-requirement-format-number",
+ ("number", value.ToString()));
}
///
diff --git a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
index ee4511acc6..388bfd1cf3 100644
--- a/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
+++ b/Resources/Locale/en-US/_DEN/requirements/formatted-fields.ftl
@@ -1,5 +1,6 @@
player-requirement-format-department = [color={$color}]{$department}[/color]
player-requirement-format-job = [color={$color}]{$job}[/color]
+player-requirement-format-number = [color=White]{$number}[/color]
player-requirement-format-species = [color=Green]{$species}[/color]
player-requirement-format-time = [color=Yellow]{$playtime}[/color]
player-requirement-format-trait = [color=LightBlue]{$trait}[/color]
From 1174b7cad30ed6eb15aa928286120d65a52a9318 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 15:22:19 -0500
Subject: [PATCH 49/54] refactor player restrictions for antags
---
Resources/Prototypes/Roles/Antags/nukeops.yml | 47 ++++++++++++++-----
.../Prototypes/Roles/Antags/revolutionary.yml | 11 +++--
Resources/Prototypes/Roles/Antags/thief.yml | 11 +++--
Resources/Prototypes/Roles/Antags/traitor.yml | 22 ++++++---
Resources/Prototypes/Roles/Antags/wizard.yml | 11 +++--
.../Prototypes/Roles/Antags/xenoborgs.yml | 28 +++++++----
Resources/Prototypes/Roles/Antags/zombie.yml | 11 +++--
7 files changed, 102 insertions(+), 39 deletions(-)
diff --git a/Resources/Prototypes/Roles/Antags/nukeops.yml b/Resources/Prototypes/Roles/Antags/nukeops.yml
index de86b70560..bf44417685 100644
--- a/Resources/Prototypes/Roles/Antags/nukeops.yml
+++ b/Resources/Prototypes/Roles/Antags/nukeops.yml
@@ -4,9 +4,14 @@
antagonist: true
setPreference: true
objective: roles-antag-nuclear-operative-objective
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 5h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 5h
+ # End DEN
guides: [ NuclearOperatives ]
- type: antag
@@ -16,11 +21,19 @@
setPreference: true
objective: roles-antag-nuclear-operative-agent-objective
requirements:
- - !type:OverallPlaytimeRequirement
- time: 5h
- - !type:RoleTimeRequirement
- role: JobChemist
- time: 3h
+ # Begin DEN: Use PlayerRequirements
+ # - !type:OverallPlaytimeRequirement
+ # time: 5h
+ # - !type:RoleTimeRequirement
+ # role: JobChemist
+ # time: 3h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 5h
+ - !type:PlayerJobPlaytimeRequirement
+ job: Chemist
+ minTime: 3h
+ # End DEN
guides: [ NuclearOperatives ]
- type: antag
@@ -29,13 +42,21 @@
antagonist: true
setPreference: true
objective: roles-antag-nuclear-operative-commander-objective
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 5h
- - !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 5h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 5h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 5h
+ - !type:PlayerDepartmentPlaytimeRequirement
department: Security
- time: 5h
+ minTime: 5h
# should be changed to nukie playtime when thats tracked (wyci)
+ # End DEN
guides: [ NuclearOperatives ]
- type: startingGear
diff --git a/Resources/Prototypes/Roles/Antags/revolutionary.yml b/Resources/Prototypes/Roles/Antags/revolutionary.yml
index 172876040a..c2f473b8b3 100644
--- a/Resources/Prototypes/Roles/Antags/revolutionary.yml
+++ b/Resources/Prototypes/Roles/Antags/revolutionary.yml
@@ -5,9 +5,14 @@
setPreference: true
objective: roles-antag-rev-head-objective
guides: [ Revolutionaries ]
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 1h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 1h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 1h
+ # End DEN
- type: antag
id: Rev
diff --git a/Resources/Prototypes/Roles/Antags/thief.yml b/Resources/Prototypes/Roles/Antags/thief.yml
index 740a7e217f..f3488df5ec 100644
--- a/Resources/Prototypes/Roles/Antags/thief.yml
+++ b/Resources/Prototypes/Roles/Antags/thief.yml
@@ -5,9 +5,14 @@
setPreference: true
objective: roles-antag-thief-objective
guides: [ Thieves ]
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 1h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 1h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 1h
+ # End DEN
- type: startingGear
id: ThiefGear
diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml
index edc130ef8b..c9da593486 100644
--- a/Resources/Prototypes/Roles/Antags/traitor.yml
+++ b/Resources/Prototypes/Roles/Antags/traitor.yml
@@ -5,9 +5,14 @@
setPreference: true
objective: roles-antag-syndicate-agent-objective
guides: [ Traitors ]
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 1h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 1h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 1h
+ # End DEN
- type: antag
id: TraitorSleeper
@@ -16,9 +21,14 @@
setPreference: true
objective: roles-antag-syndicate-agent-sleeper-objective
guides: [ Traitors ]
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 1h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 1h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 1h
+ # End DEN
# Syndicate Operative Outfit - Monkey
- type: startingGear
diff --git a/Resources/Prototypes/Roles/Antags/wizard.yml b/Resources/Prototypes/Roles/Antags/wizard.yml
index 9d528f9b79..75145aefdd 100644
--- a/Resources/Prototypes/Roles/Antags/wizard.yml
+++ b/Resources/Prototypes/Roles/Antags/wizard.yml
@@ -11,9 +11,14 @@
antagonist: true
# setPreference: true # Disabled as roundstart gamemode until reworked
objective: roles-antag-wizard-objective # TODO: maybe give random objs and stationary ones from AntagObjectives and AntagRandomObjectives
- requirements: # I hate time locked roles but this should be enough time for someone to be acclimated
- - !type:OverallPlaytimeRequirement
- time: 5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements: # I hate time locked roles but this should be enough time for someone to be acclimated
+ # - !type:OverallPlaytimeRequirement
+ # time: 5h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 5h
+ # End DEN
guides: [ Wizard ]
# See wizard_startinggear for wiz start gear options
diff --git a/Resources/Prototypes/Roles/Antags/xenoborgs.yml b/Resources/Prototypes/Roles/Antags/xenoborgs.yml
index 4e1989be8d..0a8d54e486 100644
--- a/Resources/Prototypes/Roles/Antags/xenoborgs.yml
+++ b/Resources/Prototypes/Roles/Antags/xenoborgs.yml
@@ -4,10 +4,16 @@
antagonist: true
setPreference: true
objective: roles-antag-mothership-core-objective
- requirements:
- - !type:RoleTimeRequirement
- role: JobBorg
- time: 18000 # 5 hrs
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobBorg
+ # time: 18000 # 5 hrs
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: JobBorg
+ minTime: 5h
+ # End DEN
guides: [ Xenoborgs ]
- type: antag
@@ -16,8 +22,14 @@
antagonist: true
setPreference: true
objective: roles-antag-xenoborg-objective
- requirements:
- - !type:RoleTimeRequirement
- role: JobBorg
- time: 18000 # 5 hrs
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobBorg
+ # time: 18000 # 5 hrs
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: JobBorg
+ minTime: 5h
+ # End DEN
guides: [ Xenoborgs ]
diff --git a/Resources/Prototypes/Roles/Antags/zombie.yml b/Resources/Prototypes/Roles/Antags/zombie.yml
index fa6561aa5e..2c5fa08b3e 100644
--- a/Resources/Prototypes/Roles/Antags/zombie.yml
+++ b/Resources/Prototypes/Roles/Antags/zombie.yml
@@ -5,9 +5,14 @@
setPreference: true
objective: roles-antag-initial-infected-objective
guides: [ Zombies ]
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 1h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 1h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 1h
+ # End DEN
- type: antag
id: Zombie
From ce40ea33a61afe98a5dbe9fad7239ebe01c3a9fa Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:12:10 -0500
Subject: [PATCH 50/54] migrate jobs to using playerrequirements
---
.../Roles/Jobs/Cargo/quartermaster.yml | 32 ++++++++----
.../Roles/Jobs/Cargo/salvage_specialist.yml | 14 ++++--
.../Roles/Jobs/Civilian/bartender.yml | 14 ++++--
.../Prototypes/Roles/Jobs/Civilian/chef.yml | 14 ++++--
.../Prototypes/Roles/Jobs/Civilian/lawyer.yml | 11 ++--
.../Prototypes/Roles/Jobs/Civilian/mime.yml | 11 ++--
.../Roles/Jobs/Civilian/service_worker.yml | 12 +++--
.../Prototypes/Roles/Jobs/Command/captain.yml | 50 +++++++++++++------
.../Roles/Jobs/Command/head_of_personnel.yml | 50 +++++++++++++------
.../Engineering/atmospheric_technician.yml | 12 +++--
.../Roles/Jobs/Engineering/chief_engineer.yml | 32 ++++++++----
.../Jobs/Engineering/station_engineer.yml | 14 ++++--
.../Jobs/Engineering/technical_assistant.yml | 22 +++++---
.../Prototypes/Roles/Jobs/Medical/chemist.yml | 14 ++++--
.../Jobs/Medical/chief_medical_officer.yml | 32 ++++++++----
.../Roles/Jobs/Medical/medical_doctor.yml | 14 ++++--
.../Roles/Jobs/Medical/medical_intern.yml | 16 ++++--
.../Roles/Jobs/Medical/paramedic.yml | 12 +++--
.../Prototypes/Roles/Jobs/Science/borg.yml | 25 +++++++---
.../Roles/Jobs/Science/research_assistant.yml | 16 ++++--
.../Roles/Jobs/Science/research_director.yml | 21 +++++---
.../Roles/Jobs/Science/scientist.yml | 14 ++++--
.../Roles/Jobs/Security/detective.yml | 14 ++++--
.../Roles/Jobs/Security/head_of_security.yml | 41 ++++++++++-----
.../Roles/Jobs/Security/security_cadet.yml | 22 +++++---
.../Roles/Jobs/Security/security_officer.yml | 14 ++++--
.../Prototypes/Roles/Jobs/Security/warden.yml | 23 ++++++---
.../Roles/requirement_overrides.yml | 43 ++++++++++------
28 files changed, 425 insertions(+), 184 deletions(-)
diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
index 92b9b93671..e8bc709ed3 100644
--- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
+++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
@@ -3,16 +3,28 @@
name: job-name-qm
description: job-description-qm
playTimeTracker: JobQuartermaster
- requirements:
- - !type:RoleTimeRequirement
- role: JobCargoTechnician
- time: 5h
- - !type:RoleTimeRequirement
- role: JobSalvageSpecialist
- time: 2.5h
- - !type:DepartmentTimeRequirement
- department: Cargo
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobCargoTechnician
+ # time: 5h
+ # - !type:RoleTimeRequirement
+ # role: JobSalvageSpecialist
+ # time: 2.5h
+ # - !type:DepartmentTimeRequirement
+ # department: Cargo
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: CargoTechnician
+ minTime: 5h
+ - !type:PlayerJobPlaytimeRequirement
+ job: SalvageSpecialist
+ minTime: 2.5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Cargo
+ minTime: 10h
+ # End DEN
weight: 10
startingGear: QuartermasterGear
icon: "JobIconQuarterMaster"
diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml b/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml
index 7756f8dde9..57197e2f32 100644
--- a/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml
+++ b/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml
@@ -3,10 +3,16 @@
name: job-name-salvagespec
description: job-description-salvagespec
playTimeTracker: JobSalvageSpecialist
- requirements:
- - !type:DepartmentTimeRequirement
- department: Cargo
- time: 2.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Cargo
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Cargo
+ minTime: 2.5h
+ # End DEN
icon: "JobIconShaftMiner"
startingGear: SalvageSpecialistGear
supervisors: job-supervisors-qm
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml
index 2d32b87f68..24a00314f7 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml
@@ -3,10 +3,16 @@
name: job-name-bartender
description: job-description-bartender
playTimeTracker: JobBartender
- requirements:
- - !type:DepartmentTimeRequirement
- department: Civilian
- time: 0.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Civilian
+ # time: 0.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Civilian
+ minTime: 0.5h
+ # End DEN
startingGear: BartenderGear
icon: "JobIconBartender"
supervisors: job-supervisors-hop
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml
index b3a5deab15..ff2325e06e 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml
@@ -3,10 +3,16 @@
name: job-name-chef
description: job-description-chef
playTimeTracker: JobChef
- requirements:
- - !type:DepartmentTimeRequirement
- department: Civilian
- time: 0.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Civilian
+ # time: 0.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Civilian
+ minTime: 0.5h
+ # End DEN
startingGear: ChefGear
icon: "JobIconChef"
supervisors: job-supervisors-hop
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml b/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml
index 11f07252a3..26dee4de45 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml
@@ -3,9 +3,14 @@
name: job-name-lawyer
description: job-description-lawyer
playTimeTracker: JobLawyer
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 2.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 2.5h
+ # End DEN
startingGear: LawyerGear
icon: "JobIconLawyer"
supervisors: job-supervisors-hop
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
index 8ff045173d..88ec64d7b6 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
@@ -3,9 +3,14 @@
name: job-name-mime
description: job-description-mime
playTimeTracker: JobMime
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 4h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 4h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 4h
+ # End DEN
startingGear: MimeGear
icon: "JobIconMime"
supervisors: job-supervisors-hop
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml b/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml
index 0c83971827..a04528f220 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml
@@ -3,10 +3,16 @@
name: job-name-serviceworker
description: job-description-serviceworker
playTimeTracker: JobServiceWorker
- requirements:
- - !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Civilian
+ # time: 0.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
department: Civilian
- time: 0.5h
+ minTime: 0.5h
+ # End DEN
startingGear: ServiceWorkerGear
icon: "JobIconServiceWorker"
supervisors: job-supervisors-service
diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml
index 54251b263b..63fb5803d1 100644
--- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml
+++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml
@@ -3,22 +3,40 @@
name: job-name-captain
description: job-description-captain
playTimeTracker: JobCaptain
- requirements:
- - !type:DepartmentTimeRequirement
- department: Engineering
- time: 4h
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 4h
- - !type:DepartmentTimeRequirement
- department: Science
- time: 4h
- - !type:DepartmentTimeRequirement
- department: Security
- time: 4h
- - !type:DepartmentTimeRequirement
- department: Command
- time: 4h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 4h
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 4h
+ # - !type:DepartmentTimeRequirement
+ # department: Science
+ # time: 4h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 4h
+ # - !type:DepartmentTimeRequirement
+ # department: Command
+ # time: 4h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ minTime: 4h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 4h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Science
+ minTime: 4h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 4h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Command
+ minTime: 4h
+ # End DEN
weight: 20
startingGear: CaptainGear
icon: "JobIconCaptain"
diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
index 9150ca39f3..56276d2798 100644
--- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
+++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
@@ -3,22 +3,40 @@
name: job-name-hop
description: job-description-hop
playTimeTracker: JobHeadOfPersonnel
- requirements:
- - !type:DepartmentTimeRequirement
- department: Engineering
- time: 2.5h
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 2.5h
- - !type:DepartmentTimeRequirement
- department: Science
- time: 2.5h
- - !type:DepartmentTimeRequirement
- department: Security
- time: 2.5h
- - !type:DepartmentTimeRequirement
- department: Command
- time: 2.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 2.5h
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 2.5h
+ # - !type:DepartmentTimeRequirement
+ # department: Science
+ # time: 2.5h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 2.5h
+ # - !type:DepartmentTimeRequirement
+ # department: Command
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ minTime: 2.5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 2.5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Science
+ minTime: 2.5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 2.5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Command
+ minTime: 2.5h
+ # End DEN
weight: 20
startingGear: HoPGear
icon: "JobIconHeadOfPersonnel"
diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml b/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml
index 20a6dc7ace..ba35f9f5e7 100644
--- a/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml
+++ b/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml
@@ -3,10 +3,16 @@
name: job-name-atmostech
description: job-description-atmostech
playTimeTracker: JobAtmosphericTechnician
- requirements:
- - !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
department: Engineering
- time: 2.5h
+ minTime: 2.5h
+ # End DEN
startingGear: AtmosphericTechnicianGear
icon: "JobIconAtmosphericTechnician"
supervisors: job-supervisors-ce
diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
index 561d026020..fe55c00411 100644
--- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
@@ -3,16 +3,28 @@
name: job-name-ce
description: job-description-ce
playTimeTracker: JobChiefEngineer
- requirements:
- - !type:RoleTimeRequirement
- role: JobAtmosphericTechnician
- time: 2.5h
- - !type:RoleTimeRequirement
- role: JobStationEngineer
- time: 5h
- - !type:DepartmentTimeRequirement
- department: Engineering
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobAtmosphericTechnician
+ # time: 2.5h
+ # - !type:RoleTimeRequirement
+ # role: JobStationEngineer
+ # time: 5h
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: AtmosphericTechnician
+ minTime: 2.5h
+ - !type:PlayerJobPlaytimeRequirement
+ job: StationEngineer
+ minTime: 5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ minTime: 10h
+ # End DEN
weight: 10
startingGear: ChiefEngineerGear
icon: "JobIconChiefEngineer"
diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml
index c99140b095..ca10c9b1f6 100644
--- a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml
@@ -3,10 +3,16 @@
name: job-name-engineer
description: job-description-engineer
playTimeTracker: JobStationEngineer
- requirements:
- - !type:DepartmentTimeRequirement
- department: Engineering
- time: 2.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ minTime: 2.5h
+ # End DEN
startingGear: StationEngineerGear
icon: "JobIconStationEngineer"
supervisors: job-supervisors-ce
diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml
index ee4431bded..257199a3d2 100644
--- a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml
+++ b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml
@@ -3,13 +3,21 @@
name: job-name-technical-assistant
description: job-description-technical-assistant
playTimeTracker: JobTechnicalAssistant
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 1h
- - !type:DepartmentTimeRequirement
- department: Engineering
- time: 10h
- inverted: true # stop playing intern if you're good at engineering!
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 1h
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 10h
+ # inverted: true # stop playing intern if you're good at engineering!
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ maxTime: 10h
+ # End DEN
startingGear: TechnicalAssistantGear
icon: "JobIconTechnicalAssistant"
supervisors: job-supervisors-engineering
diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml
index 6f86179015..b916c116f3 100644
--- a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml
+++ b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml
@@ -3,10 +3,16 @@
name: job-name-chemist
description: job-description-chemist
playTimeTracker: JobChemist
- requirements:
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 5h
+ # End DEN
startingGear: ChemistGear
icon: "JobIconChemist"
supervisors: job-supervisors-cmo
diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
index 5abd370503..48ff1e6814 100644
--- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
@@ -5,16 +5,28 @@
name: job-name-cmo
description: job-description-cmo
playTimeTracker: JobChiefMedicalOfficer
- requirements:
- - !type:RoleTimeRequirement
- role: JobChemist
- time: 2.5h
- - !type:RoleTimeRequirement
- role: JobMedicalDoctor
- time: 5h
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobChemist
+ # time: 2.5h
+ # - !type:RoleTimeRequirement
+ # role: JobMedicalDoctor
+ # time: 5h
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: Chemist
+ minTime: 2.5h
+ - !type:PlayerJobPlaytimeRequirement
+ job: MedicalDoctor
+ minTime: 5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 10h
+ # End DEN
weight: 10
startingGear: CMOGear
icon: "JobIconChiefMedicalOfficer"
diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml
index f012f25197..9895710266 100644
--- a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml
+++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml
@@ -3,10 +3,16 @@
name: job-name-doctor
description: job-description-doctor
playTimeTracker: JobMedicalDoctor
- requirements:
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 2.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 2.5h
+ # End DEN
startingGear: DoctorGear
icon: "JobIconMedicalDoctor"
supervisors: job-supervisors-cmo
diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml
index cc96b6bee2..c29397c3d8 100644
--- a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml
+++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml
@@ -3,11 +3,17 @@
name: job-name-intern
description: job-description-intern
playTimeTracker: JobMedicalIntern
- requirements:
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 10h
- inverted: true # stop playing intern if you're good at med!
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 10h
+ # inverted: true # stop playing intern if you're good at med!
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ maxTime: 10h
+ # End DEN
startingGear: MedicalInternGear
icon: "JobIconMedicalIntern"
supervisors: job-supervisors-medicine
diff --git a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml
index 4ef5f08e4d..10f33cf27e 100644
--- a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml
+++ b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml
@@ -3,10 +3,16 @@
name: job-name-paramedic
description: job-description-paramedic
playTimeTracker: JobParamedic
- requirements:
- - !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
department: Medical
- time: 2.5h
+ minTime: 2.5h
+ # End DEN
startingGear: ParamedicGear
icon: "JobIconParamedic"
supervisors: job-supervisors-cmo
diff --git a/Resources/Prototypes/Roles/Jobs/Science/borg.yml b/Resources/Prototypes/Roles/Jobs/Science/borg.yml
index 7834043799..adad522466 100644
--- a/Resources/Prototypes/Roles/Jobs/Science/borg.yml
+++ b/Resources/Prototypes/Roles/Jobs/Science/borg.yml
@@ -5,10 +5,16 @@
name: job-name-station-ai
description: job-description-station-ai
playTimeTracker: JobStationAi
- requirements:
- - !type:RoleTimeRequirement
- role: JobBorg
- time: 5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobBorg
+ # time: 5h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: JobBorg
+ minTime: 5h
+ # End DEN
weight: 10
canBeAntag: false
icon: JobIconStationAi
@@ -22,9 +28,14 @@
name: job-name-borg
description: job-description-borg
playTimeTracker: JobBorg
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 10h
+ # End DEN
canBeAntag: false
icon: JobIconBorg
supervisors: job-supervisors-rd
diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml
index 4805985822..70c8e41b52 100644
--- a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml
+++ b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml
@@ -3,11 +3,17 @@
name: job-name-research-assistant
description: job-description-research-assistant
playTimeTracker: JobResearchAssistant
- requirements:
- - !type:DepartmentTimeRequirement
- department: Science
- time: 10h
- inverted: true # stop playing intern if you're good at science!
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Science
+ # time: 10h
+ # inverted: true # stop playing intern if you're good at science!
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Science
+ maxTime: 10h
+ # End DEN
startingGear: ResearchAssistantGear
icon: "JobIconResearchAssistant"
supervisors: job-supervisors-science
diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml
index a7b057dc1b..6e1d67ec37 100644
--- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml
+++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml
@@ -3,13 +3,22 @@
name: job-name-rd
description: job-description-rd
playTimeTracker: JobResearchDirector
- requirements:
- - !type:RoleTimeRequirement
- role: JobScientist
- time: 5h
- - !type:DepartmentTimeRequirement
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobScientist
+ # time: 5h
+ # - !type:DepartmentTimeRequirement
+ # department: Science
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: Scientist
+ minTime: 5h
+ - !type:PlayerDepartmentPlaytimeRequirement
department: Science
- time: 10h
+ minTime: 10h
+ # End DEN
weight: 10
startingGear: ResearchDirectorGear
icon: "JobIconResearchDirector"
diff --git a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml
index 5e8e1b6e14..c1a7b6d927 100644
--- a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml
+++ b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml
@@ -3,10 +3,16 @@
name: job-name-scientist
description: job-description-scientist
playTimeTracker: JobScientist
- requirements:
- - !type:DepartmentTimeRequirement
- department: Science
- time: 2.5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Science
+ # time: 2.5h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Science
+ minTime: 2.5h
+ # End DEN
startingGear: ScientistGear
icon: "JobIconScientist"
supervisors: job-supervisors-rd
diff --git a/Resources/Prototypes/Roles/Jobs/Security/detective.yml b/Resources/Prototypes/Roles/Jobs/Security/detective.yml
index b38f2c8b1c..bff111939b 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/detective.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/detective.yml
@@ -3,10 +3,16 @@
name: job-name-detective
description: job-description-detective
playTimeTracker: JobDetective
- requirements:
- - !type:RoleTimeRequirement
- role: JobSecurityOfficer
- time: 5h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobSecurityOfficer
+ # time: 5h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: SecurityOfficer
+ minTime: 5h
+ # End DEN
startingGear: DetectiveGear
icon: "JobIconDetective"
supervisors: job-supervisors-hos
diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
index e8fdc6f738..daf6b0dff0 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
@@ -3,19 +3,34 @@
name: job-name-hos
description: job-description-hos
playTimeTracker: JobHeadOfSecurity
- requirements:
- - !type:RoleTimeRequirement
- role: JobWarden
- time: 3h
- - !type:RoleTimeRequirement
- role: JobDetective
- time: 1h # knowing how to use the tools is important
- - !type:RoleTimeRequirement
- role: JobSecurityOfficer
- time: 5h
- - !type:DepartmentTimeRequirement
- department: Security
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobWarden
+ # time: 3h
+ # - !type:RoleTimeRequirement
+ # role: JobDetective
+ # time: 1h # knowing how to use the tools is important
+ # - !type:RoleTimeRequirement
+ # role: JobSecurityOfficer
+ # time: 5h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: Detective
+ minTime: 1h
+ - !type:PlayerJobPlaytimeRequirement
+ job: SecurityOfficer
+ minTime: 5h
+ - !type:PlayerJobPlaytimeRequirement
+ job: Warden
+ minTime: 3h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 10h
+ # End DEN
weight: 10
startingGear: HoSGear
icon: "JobIconHeadOfSecurity"
diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
index 47b7472541..f6e8b0ce15 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
@@ -3,13 +3,21 @@
name: job-name-cadet
description: job-description-cadet
playTimeTracker: JobSecurityCadet
- requirements:
- - !type:OverallPlaytimeRequirement
- time: 10h
- - !type:DepartmentTimeRequirement
- department: Security
- time: 10h
- inverted: true # stop playing intern if you're good at security!
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:OverallPlaytimeRequirement
+ # time: 10h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 10h
+ # inverted: true # stop playing intern if you're good at security!
+ playerRequirements:
+ - !type:PlayerOverallPlaytimeRequirement
+ minTime: 10h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ maxTime: 10h
+ # End DEN
startingGear: SecurityCadetGear
icon: "JobIconSecurityCadet"
supervisors: job-supervisors-security
diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
index af8a274d77..627588e21d 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
@@ -3,10 +3,16 @@
name: job-name-security
description: job-description-security
playTimeTracker: JobSecurityOfficer
- requirements:
- - !type:DepartmentTimeRequirement
- department: Security
- time: 4h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 4h
+ playerRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 4h
+ # End DEN
startingGear: SecurityOfficerGear
icon: "JobIconSecurityOfficer"
supervisors: job-supervisors-hos
diff --git a/Resources/Prototypes/Roles/Jobs/Security/warden.yml b/Resources/Prototypes/Roles/Jobs/Security/warden.yml
index 2d8813a69e..2fcb39047a 100644
--- a/Resources/Prototypes/Roles/Jobs/Security/warden.yml
+++ b/Resources/Prototypes/Roles/Jobs/Security/warden.yml
@@ -3,13 +3,22 @@
name: job-name-warden
description: job-description-warden
playTimeTracker: JobWarden
- requirements:
- - !type:RoleTimeRequirement
- role: JobSecurityOfficer
- time: 5h
- - !type:DepartmentTimeRequirement
- department: Security
- time: 10h
+ # Begin DEN: Use PlayerRequirements
+ # requirements:
+ # - !type:RoleTimeRequirement
+ # role: JobSecurityOfficer
+ # time: 5h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 10h
+ playerRequirements:
+ - !type:PlayerJobPlaytimeRequirement
+ job: SecurityOfficer
+ minTime: 5h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 10h
+ # End DEN
weight: 5
startingGear: WardenGear
icon: "JobIconWarden"
diff --git a/Resources/Prototypes/Roles/requirement_overrides.yml b/Resources/Prototypes/Roles/requirement_overrides.yml
index 752249e90b..85b3651588 100644
--- a/Resources/Prototypes/Roles/requirement_overrides.yml
+++ b/Resources/Prototypes/Roles/requirement_overrides.yml
@@ -1,16 +1,31 @@
- type: jobRequirementOverride
id: Reduced
- jobs:
- Captain:
- - !type:DepartmentTimeRequirement
- department: Engineering
- time: 1h
- - !type:DepartmentTimeRequirement
- department: Medical
- time: 1h
- - !type:DepartmentTimeRequirement
- department: Security
- time: 1h
- - !type:DepartmentTimeRequirement
- department: Command
- time: 1h
+ # Begin DEN: Use PlayerRequirements
+ # jobs:
+ # Captain:
+ # - !type:DepartmentTimeRequirement
+ # department: Engineering
+ # time: 1h
+ # - !type:DepartmentTimeRequirement
+ # department: Medical
+ # time: 1h
+ # - !type:DepartmentTimeRequirement
+ # department: Security
+ # time: 1h
+ # - !type:DepartmentTimeRequirement
+ # department: Command
+ # time: 1h
+ jobRequirements:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Command
+ minTime: 1h
+ # End DEN
From 57d93182a4d94a3f987ab94ad2c651c2e712423f Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:22:20 -0500
Subject: [PATCH 51/54] whoops i forgot to put this under a Captain key
---
.../Roles/requirement_overrides.yml | 25 ++++++++++---------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/Resources/Prototypes/Roles/requirement_overrides.yml b/Resources/Prototypes/Roles/requirement_overrides.yml
index 85b3651588..e7904921bc 100644
--- a/Resources/Prototypes/Roles/requirement_overrides.yml
+++ b/Resources/Prototypes/Roles/requirement_overrides.yml
@@ -16,16 +16,17 @@
# department: Command
# time: 1h
jobRequirements:
- - !type:PlayerDepartmentPlaytimeRequirement
- department: Engineering
- minTime: 1h
- - !type:PlayerDepartmentPlaytimeRequirement
- department: Medical
- minTime: 1h
- - !type:PlayerDepartmentPlaytimeRequirement
- department: Security
- minTime: 1h
- - !type:PlayerDepartmentPlaytimeRequirement
- department: Command
- minTime: 1h
+ Captain:
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Engineering
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Medical
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Security
+ minTime: 1h
+ - !type:PlayerDepartmentPlaytimeRequirement
+ department: Command
+ minTime: 1h
# End DEN
From 24275564bbea43c0049d2a5c4d156dfe10287d7f Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:25:44 -0500
Subject: [PATCH 52/54] fix borg job IDs oops
---
Resources/Prototypes/Roles/Antags/xenoborgs.yml | 4 ++--
Resources/Prototypes/Roles/Jobs/Science/borg.yml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Resources/Prototypes/Roles/Antags/xenoborgs.yml b/Resources/Prototypes/Roles/Antags/xenoborgs.yml
index 0a8d54e486..c3c53f4dee 100644
--- a/Resources/Prototypes/Roles/Antags/xenoborgs.yml
+++ b/Resources/Prototypes/Roles/Antags/xenoborgs.yml
@@ -11,7 +11,7 @@
# time: 18000 # 5 hrs
playerRequirements:
- !type:PlayerJobPlaytimeRequirement
- job: JobBorg
+ job: Borg
minTime: 5h
# End DEN
guides: [ Xenoborgs ]
@@ -29,7 +29,7 @@
# time: 18000 # 5 hrs
playerRequirements:
- !type:PlayerJobPlaytimeRequirement
- job: JobBorg
+ job: Borg
minTime: 5h
# End DEN
guides: [ Xenoborgs ]
diff --git a/Resources/Prototypes/Roles/Jobs/Science/borg.yml b/Resources/Prototypes/Roles/Jobs/Science/borg.yml
index adad522466..3aa5cf483a 100644
--- a/Resources/Prototypes/Roles/Jobs/Science/borg.yml
+++ b/Resources/Prototypes/Roles/Jobs/Science/borg.yml
@@ -12,7 +12,7 @@
# time: 5h
playerRequirements:
- !type:PlayerJobPlaytimeRequirement
- job: JobBorg
+ job: Borg
minTime: 5h
# End DEN
weight: 10
From c41e950dbc6dde80f5b75971897bf370eaadf210 Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:42:33 -0500
Subject: [PATCH 53/54] add errors for invalid ids
---
.../PlayerRequirements/PlayerRequirements.Playtime.cs | 8 ++++----
.../PlayerRequirements/PlayerRequirements.Profile.cs | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
index 9a178972ad..0dc1326ae5 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Playtime.cs
@@ -266,7 +266,7 @@ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out stri
/// The department name of this prototype, formatted.
private string FormatDepartment(IPrototypeManager protoMan)
{
- if (!protoMan.TryIndex(Department, out var department))
+ if (!protoMan.Resolve(Department, out var department))
return Department;
var deptName = Loc.GetString(department.Name);
@@ -289,7 +289,7 @@ private string FormatDepartment(IPrototypeManager protoMan)
var playtime = TimeSpan.Zero;
if (context.Playtimes == null
- || !protoMan.TryIndex(Department, out var department))
+ || !protoMan.Resolve(Department, out var department))
return null;
// Sum the playtimes of all roles in this department.
@@ -349,7 +349,7 @@ protected override bool TryGetRangeConstraintReason([NotNullWhen(true)] out stri
/// The department name of this prototype, formatted.
private string FormatJob(IPrototypeManager protoMan)
{
- if (!protoMan.TryIndex(Job, out var job))
+ if (!protoMan.Resolve(Job, out var job))
return Job;
var jobName = Loc.GetString(job.Name);
@@ -378,7 +378,7 @@ private string FormatJob(IPrototypeManager protoMan)
var protoMan = IoCManager.Resolve();
var playtime = TimeSpan.Zero;
- if (context.Playtimes == null || !protoMan.TryIndex(Job, out var job))
+ if (context.Playtimes == null || !protoMan.Resolve(Job, out var job))
return null;
if (context.Playtimes.TryGetValue(job.PlayTimeTracker, out var tracker))
diff --git a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
index ba24121a2e..d388826c07 100644
--- a/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
+++ b/Content.Shared/_DEN/Requirements/PlayerRequirements/PlayerRequirements.Profile.cs
@@ -192,7 +192,7 @@ public override bool CheckRequirement(PlayerRequirementContext context)
private static string LocalizeSpecies(ProtoId speciesId, IPrototypeManager protoMan)
{
var speciesName = speciesId;
- if (!protoMan.TryIndex(speciesId, out var species))
+ if (!protoMan.Resolve(speciesId, out var species))
return speciesName;
speciesName = Loc.GetString(species.Name);
@@ -254,7 +254,7 @@ private static string LocalizeTrait(ProtoId traitId, IProt
{
var traitName = traitId;
- if (protoMan.TryIndex(traitId, out var trait))
+ if (protoMan.Resolve(traitId, out var trait))
traitName = Loc.GetString(trait.Name);
return Loc.GetString("player-requirement-format-trait", ("trait", traitName));
From 976dc5ad44ac6e8cfd687a8dcfdfa17d77c7246d Mon Sep 17 00:00:00 2001
From: portfiend <109661617+portfiend@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:54:03 -0500
Subject: [PATCH 54/54] fixes
---
.../Loadouts/Effects/PlayerRequirementLoadoutEffect.cs | 2 +-
Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml | 2 +-
.../Loadouts/Jobs/Engineering/station_engineer.yml | 2 +-
Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml | 4 ++--
Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml | 5 +++++
5 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
index e5d4e7b159..ef782f3bdb 100644
--- a/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
+++ b/Content.Shared/_DEN/Preferences/Loadouts/Effects/PlayerRequirementLoadoutEffect.cs
@@ -28,7 +28,7 @@ public override bool Validate(HumanoidCharacterProfile profile,
|| Requirement is PlayerPlaytimeRequirement playtimeReq && playtimeReq.ShouldAutoPass())
{
reason = FormattedMessage.Empty;
- return false;
+ return true;
}
var requirements = collection.Resolve();
diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
index 1b7c4bf524..ecd48c1153 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
@@ -5,7 +5,7 @@
# Begin DEN: Use PlayerRequirements
- !type:PlayerRequirementLoadoutEffect
requirement: !type:PlayerJobPlaytimeRequirement
- job: QuarterMaster
+ job: Quartermaster
minTime: 20h
requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
index 9c018b2725..32083b4e8e 100644
--- a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
+++ b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
@@ -5,7 +5,7 @@
# Begin DEN: Use PlayerRequirements
- !type:PlayerRequirementLoadoutEffect
requirement: !type:PlayerJobPlaytimeRequirement
- job: AtmosphericsTechnician
+ job: AtmosphericTechnician
minTime: 6h
requirementType: Loadout
- !type:PlayerRequirementLoadoutEffect
diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
index ed80248fb7..ceaaf4906b 100644
--- a/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
+++ b/Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
@@ -20,8 +20,8 @@
effects:
# Begin DEN: Use PlayerRequirements
- !type:PlayerRequirementLoadoutEffect
- requirement: !type:PlayerJobPlaytimeRequirement
- job: Cargo
+ requirement: !type:PlayerDepartmentPlaytimeRequirement
+ department: Cargo
minTime: 10h
requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
index ccb084300f..aba71486bc 100644
--- a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
+++ b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
@@ -7,6 +7,7 @@
requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Command
minTime: 1h
+ requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
# requirement:
# !type:DepartmentTimeRequirement
@@ -288,6 +289,7 @@
requirement: !type:PlayerJobPlaytimeRequirement
job: Clown
minTime: 1h
+ requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
# requirement:
# !type:RoleTimeRequirement
@@ -307,6 +309,7 @@
requirement: !type:PlayerJobPlaytimeRequirement
job: Mime
minTime: 1h
+ requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
# requirement:
# !type:RoleTimeRequirement
@@ -326,6 +329,7 @@
requirement: !type:PlayerDepartmentPlaytimeRequirement
department: Command
minTime: 5h
+ requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
# requirement:
# !type:DepartmentTimeRequirement
@@ -345,6 +349,7 @@
requirement: !type:PlayerJobPlaytimeRequirement
job: Librarian
minTime: 1h
+ requirementType: Loadout
# - !type:JobRequirementLoadoutEffect
# requirement:
# !type:RoleTimeRequirement