Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using Robust.Shared.Prototypes;

namespace Content.Shared._DEN.Requirements.Managers;

/// <summary>
/// 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.
/// </summary>
public partial record PlayerRequirementContext
{
/// <summary>
/// The currently-selected character profile of the player we are going to check.
/// </summary>
public HumanoidCharacterProfile? Profile = null;

/// <summary>
/// A dictionary of registered playtimes for the player.
/// </summary>
public Dictionary<ProtoId<PlayTimeTrackerPrototype>, TimeSpan>? Playtimes = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Content.Shared._DEN.Requirements.PlayerRequirements;
using JetBrains.Annotations;

namespace Content.Shared._DEN.Requirements.Managers;

public sealed partial class PlayerRequirementManager
{
/// <summary>
/// Check a context against enumerable requirements and gets the final pass/fail status of these requirements.
/// </summary>
/// <param name="context">The context containing fields to check against the requirements.</param>
/// <param name="requirements">An enumerable collection of requirements.</param>
/// <returns>Whether or not this context passes *all* requirements. If even one fails, then this is false.</returns>
[PublicAPI]
public static bool CheckRequirements(PlayerRequirementContext context, IEnumerable<IPlayerRequirement> 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))
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Content.Shared._DEN.Requirements.Managers;

namespace Content.Shared._DEN.Requirements.PlayerRequirements;

/// <summary>
/// This interface is used to check some parameters associated with a player (a
/// <see cref="PlayerRequirementContext"/>) to determine whether or not the player is allowed to
/// use a certain feature.
/// </summary>
/// <remarks>
/// This is a replacement of the old JobRequirement system.
/// </remarks>
[ImplicitDataDefinitionForInheritors]
public partial interface IPlayerRequirement
{
/// <summary>
/// Whether or not this requirement is inverted.
/// In all cases where this requirement should fail, it will succeed instead, and vice versa.
/// </summary>
[DataField]
bool Inverted { get; set; }

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
bool MustPassPreCheck { get; set; }

/// <summary>
/// Check if the given context has all parameters required in order to perform an actual check.
/// </summary>
/// <remarks>
/// 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
/// <see cref="MustPassPreCheck"/>.
/// </remarks>
/// <param name="context">A definition of parameters to check against the requirement.</param>
/// <returns></returns>
bool PreCheck(PlayerRequirementContext context);

/// <summary>
/// Check a context's fields against this requirement to determine if it passes or fails.
/// </summary>
/// <param name="context">A definition of parameters to check against the requirement.</param>
/// <returns>Whether or not the given context passes this requirement.</returns>
bool CheckRequirement(PlayerRequirementContext context);

/// <summary>
/// Get a localized string representing how this requirement displays to players.
/// </summary>
/// <remarks>
/// This should, ideally, display regardless if the context actually passes or not.
/// It should also display differently depending on the value of <see cref="Inverted"/>.
/// </remarks>
/// <param name="context">A definition of parameters to check against the requirement.</param>
/// <returns>An optional string representing the requirement text to display to a player.</returns>
string? GetReason(PlayerRequirementContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using Content.Shared._DEN.Requirements.Managers;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;

namespace Content.Shared._DEN.Requirements.PlayerRequirements;

/// <summary>
/// An abstract class for playtime requirements that expect a playtime to be within
/// optional minimum and maximum parameters.
/// </summary>
public abstract partial class PlayerPlaytimeRequirement : IPlayerRequirement
{
/// <inheritdoc/>
[DataField] public bool Inverted { get; set; } = false;

/// <inheritdoc/>
[DataField] public bool MustPassPreCheck { get; set; } = false;

/// <summary>
/// The minimum time you can have in this tracker.
/// </summary>
[DataField] public TimeSpan? MinTime = null;

/// <summary>
/// The maximum time you can have in this tracker.
/// </summary>
[DataField] public TimeSpan? MaxTime = null;

/// <inheritdoc/>
public bool PreCheck(PlayerRequirementContext context)
{
return context.Playtimes != null;
}

/// <inheritdoc/>
public abstract bool CheckRequirement(PlayerRequirementContext context);

/// <inheritdoc/>
public abstract string? GetReason(PlayerRequirementContext context);

/// <summary>
/// Check if a given playtime tracker fits within the minimum and maximum times of this requirement.
/// </summary>
/// <param name="playtime">The playtime to check.</param>
/// <returns>Whether or not this playtime is valid.</returns>
protected bool IsValidPlaytime(TimeSpan playtime)
{
if (MinTime != null & playtime < MinTime)
return false;

if (MaxTime != null & playtime > MaxTime)
return false;

return true;
}

/// <summary>
/// Gets a localized "reason" string for this requirement's playtime ranges.
/// </summary>
/// <remarks>
/// For example: "Less than 30 minutes", "At least 120 minutes", "Between 20 minutes and 180 minutes".
/// </remarks>
/// <returns>
/// A string describing how much playtime you should have. Null if both minimum and maximum as null.
/// </returns>
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
};
}

/// <summary>
/// Gets a localized time string for the given playtime.
/// </summary>
/// <param name="playtime">The playtime to format.</param>
/// <returns>A localized time string for this playtime.</returns>
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;
}
}

/// <summary>
/// Checks if a player's total playtime in a given department fits within a given playtime range.
/// </summary>
public sealed partial class PlayerDepartmentPlaytimeRequirement : PlayerPlaytimeRequirement
{
/// <summary>
/// The department we should check against the requirement.
/// </summary>
[DataField(required: true)]
public ProtoId<DepartmentPrototype> Department = default!;

/// <inheritdoc/>
public override bool CheckRequirement(PlayerRequirementContext context)
{
var playtime = GetDepartmentPlaytime(context);
if (playtime is null)
return false;

return IsValidPlaytime(playtime.Value);
}

/// <inheritdoc/>
public override string? GetReason(PlayerRequirementContext context)
{
// Get the department name.
var protoMan = IoCManager.Resolve<IPrototypeManager>();
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));
}

/// <summary>
/// Get the total playtime for this department.
/// </summary>
/// <param name="context">A definition of parameters to check against the requirement.</param>
/// <returns>The total playtime of this department. Null if either context playtimes or department is invalid.</returns>
private TimeSpan? GetDepartmentPlaytime(PlayerRequirementContext context)
{
var protoMan = IoCManager.Resolve<IPrototypeManager>();
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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
Loading