JobRequirement refactor (#30347)

* refactor JobRequirements

* add profile support

* fix

* Update quartermaster.yml

* sloth fixes

* inport 30208

* Update DepartmentPrototype.cs

* species restriction

* left tweak stick

* stringbuilder is cool!
This commit is contained in:
Ed
2024-08-05 07:25:49 +03:00
committed by GitHub
parent 01e1624567
commit a95d2257c3
22 changed files with 415 additions and 233 deletions

View File

@@ -25,8 +25,10 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
var manager = collection.Resolve<ISharedPlaytimeManager>();
var playtimes = manager.GetPlayTimes(session);
return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason,
collection.Resolve<IEntityManager>(),
collection.Resolve<IPrototypeManager>());
return Requirement.Check(collection.Resolve<IEntityManager>(),
collection.Resolve<IPrototypeManager>(),
profile,
playtimes,
out reason);
}
}

View File

@@ -9,10 +9,16 @@ public sealed partial class DepartmentPrototype : IPrototype
public string ID { get; } = string.Empty;
/// <summary>
/// A description string to display in the character menu as an explanation of the department's function.
/// The name LocId of the department that will be displayed in the various menus.
/// </summary>
[DataField(required: true)]
public string Description = string.Empty;
public LocId Name = string.Empty;
/// <summary>
/// A description LocId to display in the character menu as an explanation of the department's function.
/// </summary>
[DataField(required: true)]
public LocId Description = string.Empty;
/// <summary>
/// A color representing this department to use for text.

View File

@@ -0,0 +1,50 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
/// <summary>
/// Requires the character to be older or younger than a certain age (inclusive)
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class AgeRequirement : JobRequirement
{
[DataField(required: true)]
public int RequiredAge;
public override bool Check(IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = new FormattedMessage();
if (profile is null) //the profile could be null if the player is a ghost. In this case we don't need to block the role selection for ghostrole
return true;
if (!Inverted)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-young",
("age", RequiredAge)));
if (profile.Age <= RequiredAge)
return false;
}
else
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-old",
("age", RequiredAge)));
if (profile.Age >= RequiredAge)
return false;
}
return true;
}
}

View File

@@ -0,0 +1,83 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class DepartmentTimeRequirement : JobRequirement
{
/// <summary>
/// Which department needs the required amount of time.
/// </summary>
[DataField(required: true)]
public ProtoId<DepartmentPrototype> Department = default!;
/// <summary>
/// How long (in seconds) this requirement is.
/// </summary>
[DataField(required: true)]
public TimeSpan Time;
public override bool Check(IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = new FormattedMessage();
var playtime = TimeSpan.Zero;
// Check all jobs' departments
var department = protoManager.Index(Department);
var jobs = department.Roles;
string proto;
// Check all jobs' playtime
foreach (var other in jobs)
{
// The schema is stored on the Job role but we want to explode if the timer isn't found anyway.
proto = protoManager.Index(other).PlayTimeTracker;
playTimes.TryGetValue(proto, out var otherTime);
playtime += otherTime;
}
var deptDiff = Time.TotalMinutes - playtime.TotalMinutes;
var nameDepartment = "role-timer-department-unknown";
if (protoManager.TryIndex(Department, out var departmentIndexed))
{
nameDepartment = departmentIndexed.Name;
}
if (!Inverted)
{
if (deptDiff <= 0)
return true;
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-insufficient",
("time", Math.Ceiling(deptDiff)),
("department", Loc.GetString(nameDepartment)),
("departmentColor", department.Color.ToHex())));
return false;
}
if (deptDiff <= 0)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-too-high",
("time", -deptDiff),
("department", Loc.GetString(nameDepartment)),
("departmentColor", department.Color.ToHex())));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,50 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class OverallPlaytimeRequirement : JobRequirement
{
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
[DataField(required: true)]
public TimeSpan Time;
public override bool Check(IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = new FormattedMessage();
var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall);
var overallDiff = Time.TotalMinutes - overallTime.TotalMinutes;
if (!Inverted)
{
if (overallDiff <= 0 || overallTime >= Time)
return true;
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-overall-insufficient",
("time", Math.Ceiling(overallDiff))));
return false;
}
if (overallDiff <= 0 || overallTime >= Time)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high",
("time", -overallDiff)));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,73 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using Content.Shared.Roles.Jobs;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class RoleTimeRequirement : JobRequirement
{
/// <summary>
/// What particular role they need the time requirement with.
/// </summary>
[DataField(required: true)]
public ProtoId<PlayTimeTrackerPrototype> Role = default!;
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
[DataField(required: true)]
public TimeSpan Time;
public override bool Check(IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = new FormattedMessage();
string proto = Role;
playTimes.TryGetValue(proto, out var roleTime);
var roleDiff = Time.TotalMinutes - roleTime.TotalMinutes;
var departmentColor = Color.Yellow;
if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
{
var jobProto = jobSystem.GetJobPrototype(proto);
if (jobSystem.TryGetDepartment(jobProto, out var departmentProto))
departmentColor = departmentProto.Color;
}
if (!Inverted)
{
if (roleDiff <= 0)
return true;
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-insufficient",
("time", Math.Ceiling(roleDiff)),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;
}
if (roleDiff <= 0)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-too-high",
("time", -roleDiff),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,59 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
/// <summary>
/// Requires the character to be or not be on the list of specified species
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class SpeciesRequirement : JobRequirement
{
[DataField(required: true)]
public HashSet<ProtoId<SpeciesPrototype>> Species = new();
public override bool Check(IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = new FormattedMessage();
if (profile is null) //the profile could be null if the player is a ghost. In this case we don't need to block the role selection for ghostrole
return true;
var sb = new StringBuilder();
sb.Append("[color=yellow]");
foreach (var s in Species)
{
sb.Append(Loc.GetString(protoManager.Index(s).Name) + " ");
}
sb.Append("[/color]");
if (!Inverted)
{
reason = FormattedMessage.FromMarkupPermissive($"{Loc.GetString("role-timer-whitelisted-species")}\n{sb}");
if (!Species.Contains(profile.Species))
return false;
}
else
{
reason = FormattedMessage.FromMarkupPermissive($"{Loc.GetString("role-timer-blacklisted-species")}\n{sb}");
if (Species.Contains(profile.Species))
return false;
}
return true;
}
}

View File

@@ -1,228 +1,51 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles.Jobs;
using JetBrains.Annotations;
using Content.Shared.Preferences;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Shared.Roles
namespace Content.Shared.Roles;
public static class JobRequirements
{
/// <summary>
/// Abstract class for playtime and other requirements for role gates.
/// </summary>
[ImplicitDataDefinitionForInheritors]
[Serializable, NetSerializable]
public abstract partial class JobRequirement{}
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class DepartmentTimeRequirement : JobRequirement
public static bool TryRequirementsMet(
JobPrototype job,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile)
{
/// <summary>
/// Which department needs the required amount of time.
/// </summary>
[DataField("department", customTypeSerializer: typeof(PrototypeIdSerializer<DepartmentPrototype>))]
public string Department = default!;
/// <summary>
/// How long (in seconds) this requirement is.
/// </summary>
[DataField("time")] public TimeSpan Time;
/// <summary>
/// If true, requirement will return false if playtime above the specified time.
/// </summary>
/// <value>
/// <c>False</c> by default.<br />
/// <c>True</c> for invert general requirement
/// </value>
[DataField("inverted")] public bool Inverted;
}
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class RoleTimeRequirement : JobRequirement
{
/// <summary>
/// What particular role they need the time requirement with.
/// </summary>
[DataField("role", customTypeSerializer: typeof(PrototypeIdSerializer<PlayTimeTrackerPrototype>))]
public string Role = default!;
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
[DataField("time")] public TimeSpan Time;
/// <inheritdoc cref="DepartmentTimeRequirement.Inverted"/>
[DataField("inverted")] public bool Inverted;
}
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class OverallPlaytimeRequirement : JobRequirement
{
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
[DataField("time")] public TimeSpan Time;
/// <inheritdoc cref="DepartmentTimeRequirement.Inverted"/>
[DataField("inverted")] public bool Inverted;
}
public static class JobRequirements
{
public static bool TryRequirementsMet(
JobPrototype job,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager prototypes)
{
var sys = entManager.System<SharedRoleSystem>();
var requirements = sys.GetJobRequirement(job);
reason = null;
if (requirements == null)
return true;
foreach (var requirement in requirements)
{
if (!TryRequirementMet(requirement, playTimes, out reason, entManager, prototypes))
return false;
}
var sys = entManager.System<SharedRoleSystem>();
var requirements = sys.GetJobRequirement(job);
reason = null;
if (requirements == null)
return true;
}
/// <summary>
/// Returns a string with the reason why a particular requirement may not be met.
/// </summary>
public static bool TryRequirementMet(
JobRequirement requirement,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager prototypes)
foreach (var requirement in requirements)
{
reason = null;
switch (requirement)
{
case DepartmentTimeRequirement deptRequirement:
var playtime = TimeSpan.Zero;
// Check all jobs' departments
var department = prototypes.Index<DepartmentPrototype>(deptRequirement.Department);
var jobs = department.Roles;
string proto;
// Check all jobs' playtime
foreach (var other in jobs)
{
// The schema is stored on the Job role but we want to explode if the timer isn't found anyway.
proto = prototypes.Index<JobPrototype>(other).PlayTimeTracker;
playTimes.TryGetValue(proto, out var otherTime);
playtime += otherTime;
}
var deptDiff = deptRequirement.Time.TotalMinutes - playtime.TotalMinutes;
if (!deptRequirement.Inverted)
{
if (deptDiff <= 0)
return true;
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-insufficient",
("time", Math.Ceiling(deptDiff)),
("department", Loc.GetString(deptRequirement.Department)),
("departmentColor", department.Color.ToHex())));
return false;
}
else
{
if (deptDiff <= 0)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-too-high",
("time", -deptDiff),
("department", Loc.GetString(deptRequirement.Department)),
("departmentColor", department.Color.ToHex())));
return false;
}
return true;
}
case OverallPlaytimeRequirement overallRequirement:
var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall);
var overallDiff = overallRequirement.Time.TotalMinutes - overallTime.TotalMinutes;
if (!overallRequirement.Inverted)
{
if (overallDiff <= 0 || overallTime >= overallRequirement.Time)
return true;
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-overall-insufficient",
("time", Math.Ceiling(overallDiff))));
return false;
}
else
{
if (overallDiff <= 0 || overallTime >= overallRequirement.Time)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high", ("time", -overallDiff)));
return false;
}
return true;
}
case RoleTimeRequirement roleRequirement:
proto = roleRequirement.Role;
playTimes.TryGetValue(proto, out var roleTime);
var roleDiff = roleRequirement.Time.TotalMinutes - roleTime.TotalMinutes;
var departmentColor = Color.Yellow;
if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
{
var jobProto = jobSystem.GetJobPrototype(proto);
if (jobSystem.TryGetDepartment(jobProto, out var departmentProto))
departmentColor = departmentProto.Color;
}
if (!roleRequirement.Inverted)
{
if (roleDiff <= 0)
return true;
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-insufficient",
("time", Math.Ceiling(roleDiff)),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;
}
else
{
if (roleDiff <= 0)
{
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-too-high",
("time", -roleDiff),
("job", Loc.GetString(proto)),
("departmentColor", departmentColor.ToHex())));
return false;
}
return true;
}
default:
throw new NotImplementedException();
}
if (!requirement.Check(entManager, protoManager, profile, playTimes, out reason))
return false;
}
return true;
}
}
/// <summary>
/// Abstract class for playtime and other requirements for role gates.
/// </summary>
[ImplicitDataDefinitionForInheritors]
[Serializable, NetSerializable]
public abstract partial class JobRequirement
{
[DataField]
public bool Inverted;
public abstract bool Check(
IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason);
}