From 39ca6175e60c9bb40001bbe8b1ba5ac7189e56da Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Thu, 17 Apr 2025 19:55:25 +0300 Subject: [PATCH] Simple sponsorship system (#1189) * simple sponsorship system * priority Join * OOC Sponsor Color * Update CP14SponsorRolePrototype.cs * loadout sponsorship * bruh * refactor to interfaces * Update CP14ClientSponsorManager.cs * finish loadout option * role pass --- Content.Client/Entry/EntryPoint.cs | 3 + Content.Client/IoC/ClientContentIoC.cs | 3 + .../JobRequirementsManager.cs | 3 +- .../_CP14/Sponsor/CP14ClientSponsorManager.cs | 49 ++++++ Content.Server/Chat/Managers/ChatManager.cs | 8 + .../Connection/ConnectionManager.cs | 7 +- Content.Server/Entry/EntryPoint.cs | 3 + Content.Server/IoC/ServerContentIoC.cs | 3 + .../PlayTimeTrackingSystem.cs | 6 +- .../_CP14/Discord/DiscordAuthManager.cs | 8 +- .../_CP14/JoinQueue/JoinQueueManager.cs | 1 - .../_CP14/Sponsor/CP14SponsorManager.cs | 156 ++++++++++++++++++ .../Effects/JobRequirementLoadoutEffect.cs | 4 +- .../Roles/JobRequirement/AgeRequirement.cs | 4 +- .../DepartmentTimeRequirement.cs | 4 +- .../OverallPlaytimeRequirement.cs | 4 +- .../JobRequirement/RoleTimeRequirement.cs | 4 +- .../JobRequirement/SpeciesRequirement.cs | 4 +- .../Roles/JobRequirement/TraitsRequirement.cs | 4 +- Content.Shared/Roles/JobRequirements.cs | 5 +- .../_CP14/CCvar/CCvars.CP14Sponsors.cs | 3 + .../_CP14/Roles/CP14SponsorFeatureRequired.cs | 74 +++++++++ .../_CP14/Sponsor/CP14SharedSponsorManager.cs | 35 ++++ .../_CP14/Sponsor/CP14SponsorRolePrototype.cs | 33 ++++ Resources/ConfigPresets/_CP14/Dev.toml | 3 +- .../Locale/en-US/_CP14/job/requirements.ftl | 1 + .../Locale/ru-RU/_CP14/job/requirements.ftl | 1 + .../Prototypes/_CP14/Sponsor/features.yml | 3 + Resources/Prototypes/_CP14/Sponsor/roles.yml | 76 +++++++++ 29 files changed, 491 insertions(+), 21 deletions(-) create mode 100644 Content.Client/_CP14/Sponsor/CP14ClientSponsorManager.cs create mode 100644 Content.Server/_CP14/Sponsor/CP14SponsorManager.cs create mode 100644 Content.Shared/_CP14/Roles/CP14SponsorFeatureRequired.cs create mode 100644 Content.Shared/_CP14/Sponsor/CP14SharedSponsorManager.cs create mode 100644 Content.Shared/_CP14/Sponsor/CP14SponsorRolePrototype.cs create mode 100644 Resources/Locale/en-US/_CP14/job/requirements.ftl create mode 100644 Resources/Locale/ru-RU/_CP14/job/requirements.ftl create mode 100644 Resources/Prototypes/_CP14/Sponsor/features.yml create mode 100644 Resources/Prototypes/_CP14/Sponsor/roles.yml diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 997cfc0b00..9481e2fb7b 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -24,6 +24,7 @@ using Content.Client.Singularity; using Content.Client.Stylesheets; using Content.Client.Viewport; using Content.Client.Voting; +using Content.Shared._CP14.Sponsor; using Content.Shared.Ame.Components; using Content.Shared.CCVar; using Content.Shared.Gravity; @@ -48,6 +49,7 @@ namespace Content.Client.Entry //CP14 [Dependency] private readonly DiscordAuthManager _discordAuth = default!; [Dependency] private readonly JoinQueueManager _joinQueueManager = default!; + [Dependency] private readonly ICP14SponsorManager _sponsorManager = default!; //CP14 end [Dependency] private readonly IBaseClient _baseClient = default!; [Dependency] private readonly IGameController _gameController = default!; @@ -170,6 +172,7 @@ namespace Content.Client.Entry _overlayManager.AddOverlay(new CP14BasePostProcessOverlay()); _discordAuth.Initialize(); _joinQueueManager.Initialize(); + _sponsorManager.Initialize(); //CP14 end _overlayManager.AddOverlay(new SingularityOverlay()); _overlayManager.AddOverlay(new RadiationPulseOverlay()); diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index ebb893c6a5..98d97b55d4 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -1,5 +1,6 @@ using Content.Client._CP14.Discord; using Content.Client._CP14.JoinQueue; +using Content.Client._CP14.Sponsor; using Content.Client.Administration.Managers; using Content.Client.Changelog; using Content.Client.Chat.Managers; @@ -22,6 +23,7 @@ using Content.Client.Voting; using Content.Shared.Administration.Logs; using Content.Client.Lobby; using Content.Client.Players.RateLimiting; +using Content.Shared._CP14.Sponsor; using Content.Shared.Administration.Managers; using Content.Shared.Chat; using Content.Shared.Players.PlayTimeTracking; @@ -38,6 +40,7 @@ namespace Content.Client.IoC //CP14 collection.Register(); collection.Register(); + collection.Register(); //CP14 end collection.Register(); collection.Register(); diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 314b59eda9..6e2db23929 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -126,7 +126,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager var reasons = new List(); foreach (var requirement in requirements) { - if (requirement.Check(_entManager, _prototypes, profile, _roles, out var jobReason)) + //CP14 Add NetUserId for sponsorship checks + if (requirement.Check(_playerManager.LocalSession?.UserId, _entManager, _prototypes, profile, _roles, out var jobReason)) continue; reasons.Add(jobReason.ToMarkup()); diff --git a/Content.Client/_CP14/Sponsor/CP14ClientSponsorManager.cs b/Content.Client/_CP14/Sponsor/CP14ClientSponsorManager.cs new file mode 100644 index 0000000000..a1568107f6 --- /dev/null +++ b/Content.Client/_CP14/Sponsor/CP14ClientSponsorManager.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._CP14.Sponsor; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Client._CP14.Sponsor; + +public sealed class ClientSponsorSystem : ICP14SponsorManager +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IClientNetManager _net = default!; + + private CP14SponsorRolePrototype? _sponsorRole; + + public void Initialize() + { + _net.RegisterNetMessage(OnSponsorRoleUpdate); + _net.Disconnect += NetOnDisconnected; + } + + private void NetOnDisconnected(object? sender, NetDisconnectedArgs e) + { + _sponsorRole = null; + } + + private void OnSponsorRoleUpdate(CP14SponsorRoleUpdate msg) + { + if (!_proto.TryIndex(msg.Role, out var indexedRole)) + return; + + _sponsorRole = indexedRole; + } + + public bool TryGetSponsorOOCColor(NetUserId userId, [NotNullWhen(true)] out Color? color) + { + throw new NotImplementedException(); + } + + public bool UserHasFeature(NetUserId userId, ProtoId feature, bool ifDisabledSponsorhip = true) + { + if (_sponsorRole is null) + return false; + + if (!_proto.TryIndex(feature, out var indexedFeature)) + return false; + + return _sponsorRole.Priority >= indexedFeature.MinPriority; + } +} diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index b5859c7ef6..931fda0741 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -7,6 +7,7 @@ using Content.Server.Administration.Systems; using Content.Server.MoMMI; using Content.Server.Players.RateLimiting; using Content.Server.Preferences.Managers; +using Content.Shared._CP14.Sponsor; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Chat; @@ -44,6 +45,7 @@ internal sealed partial class ChatManager : IChatManager [Dependency] private readonly INetConfigurationManager _netConfigManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!; + [Dependency] private readonly ICP14SponsorManager _sponsor = default!; //CP14 OCC color /// /// The maximum length a player-sent message can be sent @@ -256,6 +258,12 @@ internal sealed partial class ChatManager : IChatManager { wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message))); } + //CP14 Sponsor OOC Color + if (_sponsor.TryGetSponsorOOCColor(player.UserId, out var oocColor)) + { + wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", oocColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message))); + } + //CP14 Sponsor OOC Color end //TODO: player.Name color, this will need to change the structure of the MsgChatMessage ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId); diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index a92862cf1d..be52275a0d 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -2,12 +2,14 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using System.Runtime.InteropServices; +using Content.Server._CP14.Sponsor; using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.Connection.IPIntel; using Content.Server.Database; using Content.Server.GameTicking; using Content.Server.Preferences.Managers; +using Content.Shared._CP14.Sponsor; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Players.PlayTimeTracking; @@ -64,6 +66,7 @@ namespace Content.Server.Connection [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IHttpClientHolder _http = default!; [Dependency] private readonly IAdminManager _adminManager = default!; + [Dependency] private readonly ICP14SponsorManager _sponsor = default!; //CP14 Priority Join private ISawmill _sawmill = default!; private readonly Dictionary _temporaryBypasses = []; @@ -375,11 +378,11 @@ namespace Content.Server.Connection public async Task HavePrivilegedJoin(NetUserId userId) { var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && await _db.GetAdminDataForAsync(userId) != null; - //var havePriorityJoin = _sponsors + var havePriorityJoin = _sponsor.UserHasFeature(userId, "PriorityJoin", false); var wasInGame = EntitySystem.TryGet(out var ticker) && ticker.PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame; - return adminBypass || wasInGame; + return adminBypass || wasInGame || havePriorityJoin; } //CP14 Join Queue end } diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index eae64fa461..82fae10eb4 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -1,5 +1,6 @@ using Content.Server._CP14.Discord; using Content.Server._CP14.JoinQueue; +using Content.Server._CP14.Sponsor; using Content.Server.Acz; using Content.Server.Administration; using Content.Server.Administration.Logs; @@ -25,6 +26,7 @@ using Content.Server.Preferences.Managers; using Content.Server.ServerInfo; using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; +using Content.Shared._CP14.Sponsor; using Content.Shared.CCVar; using Content.Shared.Kitchen; using Content.Shared.Localizations; @@ -106,6 +108,7 @@ namespace Content.Server.Entry //CP14 IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); //CP14 end IoCManager.Resolve().Initialize(); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 846ec7e557..ddc59b6faa 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -1,5 +1,6 @@ using Content.Server._CP14.Discord; using Content.Server._CP14.JoinQueue; +using Content.Server._CP14.Sponsor; using Content.Server.Administration; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; @@ -25,6 +26,7 @@ using Content.Server.ServerInfo; using Content.Server.ServerUpdates; using Content.Server.Voting.Managers; using Content.Server.Worldgen.Tools; +using Content.Shared._CP14.Sponsor; using Content.Shared.Administration.Logs; using Content.Shared.Administration.Managers; using Content.Shared.Chat; @@ -41,6 +43,7 @@ namespace Content.Server.IoC //CP14 IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); //CP14 end IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs index b8c0d3f450..1fd4af9da6 100644 --- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs +++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs @@ -201,7 +201,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem playTimes = new Dictionary(); } - return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter); + return JobRequirements.TryRequirementsMet(player.UserId, job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter); //CP14 add NetUserId for sponsorship checks } public HashSet> GetDisallowedJobs(ICommonSession player) @@ -218,7 +218,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem foreach (var job in _prototypes.EnumeratePrototypes()) { - if (JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter)) + if (JobRequirements.TryRequirementsMet(player.UserId, job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(player.UserId).SelectedCharacter)) //CP14 add NetUserId for sponsorship checks roles.Add(job.ID); } @@ -241,7 +241,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem for (var i = 0; i < jobs.Count; i++) { if (_prototypes.TryIndex(jobs[i], out var job) - && JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(userId).SelectedCharacter)) + && JobRequirements.TryRequirementsMet(userId, job, playTimes, out _, EntityManager, _prototypes, (HumanoidCharacterProfile?) _preferencesManager.GetPreferences(userId).SelectedCharacter)) //CP14 add NetUserId for sponsorship checks { continue; } diff --git a/Content.Server/_CP14/Discord/DiscordAuthManager.cs b/Content.Server/_CP14/Discord/DiscordAuthManager.cs index 4ae44b8dbc..f2202fe8f4 100644 --- a/Content.Server/_CP14/Discord/DiscordAuthManager.cs +++ b/Content.Server/_CP14/Discord/DiscordAuthManager.cs @@ -30,11 +30,10 @@ public sealed class DiscordAuthManager private string _apiUrl = string.Empty; private string _apiKey = string.Empty; - private string _discordGuild = "1221923073759121468"; //CrystallEdge server required + public const string DISCORD_GUILD = "1221923073759121468"; //CrystallEdge server required private HashSet _blockedGuilds = new() { - "1361786483073093673", //Testing one "1346922008000204891", "1186566619858731038", "1355279097906855968", @@ -137,7 +136,6 @@ public sealed class DiscordAuthManager request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); var response = await _httpClient.SendAsync(request, cancel); - _sawmill.Debug($"Guilds response: {await response.Content.ReadAsStringAsync(cancel)}"); _sawmill.Debug($"(int) response.StatusCode: {(int)response.StatusCode}"); if (!response.IsSuccessStatusCode) { @@ -161,9 +159,9 @@ public sealed class DiscordAuthManager } } - if (guilds.Guilds.All(guild => guild.Id != _discordGuild)) + if (guilds.Guilds.All(guild => guild.Id != DISCORD_GUILD)) { - _sawmill.Debug($"Player {userId} is not in required guild {_discordGuild}"); + _sawmill.Debug($"Player {userId} is not in required guild {DISCORD_GUILD}"); return new AuthData { Verified = false, ErrorMessage = "You are not a member of the CrystallEdge server." }; } diff --git a/Content.Server/_CP14/JoinQueue/JoinQueueManager.cs b/Content.Server/_CP14/JoinQueue/JoinQueueManager.cs index 97a2f407ca..0abad924f0 100644 --- a/Content.Server/_CP14/JoinQueue/JoinQueueManager.cs +++ b/Content.Server/_CP14/JoinQueue/JoinQueueManager.cs @@ -40,7 +40,6 @@ public sealed class JoinQueueManager [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly DiscordAuthManager _discordAuthManager = default!; - //[Dependency] private readonly SponsorsManager _sponsors = default!; private ISawmill _sawmill = default!; /// diff --git a/Content.Server/_CP14/Sponsor/CP14SponsorManager.cs b/Content.Server/_CP14/Sponsor/CP14SponsorManager.cs new file mode 100644 index 0000000000..caf6a814ef --- /dev/null +++ b/Content.Server/_CP14/Sponsor/CP14SponsorManager.cs @@ -0,0 +1,156 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Content.Server._CP14.Discord; +using Content.Shared._CP14.Sponsor; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server._CP14.Sponsor; + +public sealed class SponsorSystem : ICP14SponsorManager +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly DiscordAuthManager _discordAuthManager = default!; + [Dependency] private readonly INetManager _netMgr = default!; + [Dependency] private readonly IServerNetManager _netManager = default!; + + private readonly HttpClient _httpClient = new(); + private string _apiUrl = string.Empty; + private string _apiKey = string.Empty; + private bool _enabled; + + private ISawmill _sawmill = null!; + + private Dictionary _cachedSponsors = new(); + + public void Initialize() + { + _sawmill = Logger.GetSawmill("sponsors"); + + _netManager.RegisterNetMessage(); + + _cfg.OnValueChanged(CCVars.SponsorsEnabled, val => { _enabled = val; }, true); + _cfg.OnValueChanged(CCVars.SponsorsApiUrl, val => { _apiUrl = val; }, true); + _cfg.OnValueChanged(CCVars.SponsorsApiKey, val => { _apiKey = val; }, true); + + _discordAuthManager.PlayerVerified += async (_, e) => + { + await OnPlayerVerified(e); + }; + + _netMgr.Disconnect += OnDisconnect; + + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", _apiKey); + } + + private async Task?> GetRoles(NetUserId userId) + { + var requestUrl = $"{_apiUrl}/api/roles?method=uid&id={userId}&guildId={DiscordAuthManager.DISCORD_GUILD}"; + var response = await _httpClient.GetAsync(requestUrl); + + if (!response.IsSuccessStatusCode) + { + _sawmill.Error($"Failed to retrieve roles for user {userId}: {response.StatusCode}"); + return null; + } + + var responseContent = await response.Content.ReadFromJsonAsync(); + + if (responseContent is not null) + { + return responseContent.Roles.ToList(); + } + + _sawmill.Error($"Roles not found in response for user {userId}"); + return null; + } + + private async Task OnPlayerVerified(ICommonSession e) + { + if (!_enabled) + return; + + var roles = await GetRoles(e.UserId); + if (roles is null) + return; + + CP14SponsorRolePrototype? targetRole = null; + foreach (var role in _proto.EnumeratePrototypes()) + { + if (!roles.Contains(role.DiscordRoleId)) + continue; + + if (targetRole is null || role.Priority > targetRole.Priority) + { + targetRole = role; + } + } + + if (targetRole is not null) + _cachedSponsors[e.UserId] = targetRole; + + if (_cachedSponsors.TryGetValue(e.UserId, out var cachedRole)) + { + e.Channel.SendMessage(new CP14SponsorRoleUpdate + { + Role = cachedRole, + }); + } + } + + private void OnDisconnect(object? sender, NetDisconnectedArgs e) + { + //Remove cached roles + if (_cachedSponsors.ContainsKey(e.Channel.UserId)) + { + _cachedSponsors.Remove(e.Channel.UserId); + } + } + + public bool UserHasFeature(NetUserId userId, + ProtoId feature, + bool ifDisabledSponsorhip = true) + { + if (!_enabled) + return ifDisabledSponsorhip; + + if (!_proto.TryIndex(feature, out var indexedFeature)) + return false; + + if (!_cachedSponsors.TryGetValue(userId, out var userRoles)) + return false; + + return _cachedSponsors[userId].Priority >= indexedFeature.MinPriority; + } + + public bool TryGetSponsorOOCColor(NetUserId userId, [NotNullWhen(true)] out Color? color) + { + color = null; + + if (!_enabled) + return false; + + if (!_cachedSponsors.TryGetValue(userId, out var sponsorRole)) + return false; + + color = sponsorRole.Color; + + return color is not null; + } + + private sealed class RolesResponse + { + [JsonPropertyName("roles")] + public string[] Roles { get; set; } = []; + } +} diff --git a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs index 2f7e7b7c48..b029345b4c 100644 --- a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs +++ b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs @@ -25,7 +25,9 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect var manager = collection.Resolve(); var playtimes = manager.GetPlayTimes(session); - return Requirement.Check(collection.Resolve(), + return Requirement.Check( + session.UserId, //CP14 add NetUserId for sponsorship checks + collection.Resolve(), collection.Resolve(), profile, playtimes, diff --git a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs index 30f607adf7..6b35a14b7a 100644 --- a/Content.Shared/Roles/JobRequirement/AgeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/AgeRequirement.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Preferences; using JetBrains.Annotations; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -17,7 +18,8 @@ public sealed partial class AgeRequirement : JobRequirement [DataField(required: true)] public int RequiredAge; - public override bool Check(IEntityManager entManager, + public override bool Check(NetUserId? userId, //CP14 Sponsorship Checks + IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, IReadOnlyDictionary playTimes, diff --git a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs index 8c86299210..5286d23276 100644 --- a/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/DepartmentTimeRequirement.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Localizations; using Content.Shared.Preferences; using JetBrains.Annotations; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -24,7 +25,8 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement [DataField(required: true)] public TimeSpan Time; - public override bool Check(IEntityManager entManager, + public override bool Check(NetUserId? userId, //CP14 Sponsorship Checks + IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, IReadOnlyDictionary playTimes, diff --git a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs index 67b3938e1a..5e3f77fa71 100644 --- a/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/OverallPlaytimeRequirement.cs @@ -3,6 +3,7 @@ using Content.Shared.Localizations; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Preferences; using JetBrains.Annotations; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -17,7 +18,8 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement [DataField(required: true)] public TimeSpan Time; - public override bool Check(IEntityManager entManager, + public override bool Check(NetUserId? userId, //CP14 Sponsorship Checks + IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, IReadOnlyDictionary playTimes, diff --git a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs index f096cfcb42..4066291e9a 100644 --- a/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs @@ -4,6 +4,7 @@ using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Preferences; using Content.Shared.Roles.Jobs; using JetBrains.Annotations; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -24,7 +25,8 @@ public sealed partial class RoleTimeRequirement : JobRequirement [DataField(required: true)] public TimeSpan Time; - public override bool Check(IEntityManager entManager, + public override bool Check(NetUserId? userId, //CP14 Sponsorship Checks + IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, IReadOnlyDictionary playTimes, diff --git a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs index 68c069931f..6152ff0048 100644 --- a/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/SpeciesRequirement.cs @@ -3,6 +3,7 @@ using System.Text; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; using JetBrains.Annotations; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -19,7 +20,8 @@ public sealed partial class SpeciesRequirement : JobRequirement [DataField(required: true)] public HashSet> Species = new(); - public override bool Check(IEntityManager entManager, + public override bool Check(NetUserId? userId, //CP14 Sponsorship Checks + IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, IReadOnlyDictionary playTimes, diff --git a/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs b/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs index 418afba159..10674ad44d 100644 --- a/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs +++ b/Content.Shared/Roles/JobRequirement/TraitsRequirement.cs @@ -4,6 +4,7 @@ using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; using Content.Shared.Traits; using JetBrains.Annotations; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -20,7 +21,8 @@ public sealed partial class TraitsRequirement : JobRequirement [DataField(required: true)] public HashSet> Traits = new(); - public override bool Check(IEntityManager entManager, + public override bool Check(NetUserId? userId, //CP14 Sponsorship Checks + IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, IReadOnlyDictionary playTimes, diff --git a/Content.Shared/Roles/JobRequirements.cs b/Content.Shared/Roles/JobRequirements.cs index 17f5f7bd6a..bc78bf1891 100644 --- a/Content.Shared/Roles/JobRequirements.cs +++ b/Content.Shared/Roles/JobRequirements.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Preferences; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -9,6 +10,7 @@ namespace Content.Shared.Roles; public static class JobRequirements { public static bool TryRequirementsMet( + NetUserId userId, //CP14 add NetUserId for sponsorship checks JobPrototype job, IReadOnlyDictionary playTimes, [NotNullWhen(false)] out FormattedMessage? reason, @@ -24,7 +26,7 @@ public static class JobRequirements foreach (var requirement in requirements) { - if (!requirement.Check(entManager, protoManager, profile, playTimes, out reason)) + if (!requirement.Check(userId, entManager, protoManager, profile, playTimes, out reason)) //CP14 add NetUserId for sponsorship checks return false; } @@ -43,6 +45,7 @@ public abstract partial class JobRequirement public bool Inverted; public abstract bool Check( + NetUserId? userId, //CP14 Sponsorship Checks IEntityManager entManager, IPrototypeManager protoManager, HumanoidCharacterProfile? profile, diff --git a/Content.Shared/_CP14/CCvar/CCvars.CP14Sponsors.cs b/Content.Shared/_CP14/CCvar/CCvars.CP14Sponsors.cs index f9e3d21e9a..9d45164f9e 100644 --- a/Content.Shared/_CP14/CCvar/CCvars.CP14Sponsors.cs +++ b/Content.Shared/_CP14/CCvar/CCvars.CP14Sponsors.cs @@ -4,6 +4,9 @@ namespace Content.Shared.CCVar; public sealed partial class CCVars { + public static readonly CVarDef SponsorsEnabled = + CVarDef.Create("cp14.sponsor_enabled", false, CVar.SERVERONLY); + public static readonly CVarDef SponsorsApiUrl = CVarDef.Create("cp14.sponsor_api_url", "http://localhost:8000/sponsors", CVar.SERVERONLY | CVar.CONFIDENTIAL); diff --git a/Content.Shared/_CP14/Roles/CP14SponsorFeatureRequired.cs b/Content.Shared/_CP14/Roles/CP14SponsorFeatureRequired.cs new file mode 100644 index 0000000000..f2612e5821 --- /dev/null +++ b/Content.Shared/_CP14/Roles/CP14SponsorFeatureRequired.cs @@ -0,0 +1,74 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._CP14.Sponsor; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared._CP14.Roles; + +/// +/// Requires a character to have, or not have, certain traits +/// +[UsedImplicitly] +[Serializable, NetSerializable] +public sealed partial class CP14SponsorFeatureRequired : JobRequirement +{ + [DataField(required: true)] + public ProtoId Feature = string.Empty; + + public override bool Check(NetUserId? userId, + IEntityManager entManager, + IPrototypeManager protoManager, + HumanoidCharacterProfile? profile, + IReadOnlyDictionary playTimes, + [NotNullWhen(false)] out FormattedMessage? reason) + { + reason = new FormattedMessage(); + + if (userId is null) + return false; + + var sponsorship = IoCManager.Resolve(); + + var haveFeature = sponsorship.UserHasFeature(userId.Value, Feature); + + if (haveFeature) + return true; + + var prototypeMan = IoCManager.Resolve(); + + var indexedFeature = prototypeMan.Index(Feature); + var lowestRole = GetLowestPriorityRole(indexedFeature.MinPriority, prototypeMan); + prototypeMan.TryIndex(lowestRole, out var indexedRole); + + if (indexedRole == null) + return false; + reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("cp14-role-req-sponsor-feature-req", ("role", indexedRole.Name))); + + return false; + } + + public ProtoId? GetLowestPriorityRole(float priority, IPrototypeManager protoMan) + { + ProtoId? lowestRole = null; + float lowestPriority = float.MaxValue; + + foreach (var role in protoMan.EnumeratePrototypes()) + { + if (!role.Examinable) + continue; + + if (role.Priority >= priority && role.Priority < lowestPriority) + { + lowestPriority = role.Priority; + lowestRole = role.ID; + } + } + + return lowestRole; + } +} diff --git a/Content.Shared/_CP14/Sponsor/CP14SharedSponsorManager.cs b/Content.Shared/_CP14/Sponsor/CP14SharedSponsorManager.cs new file mode 100644 index 0000000000..227cde6f61 --- /dev/null +++ b/Content.Shared/_CP14/Sponsor/CP14SharedSponsorManager.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._CP14.Sponsor; + +public interface ICP14SponsorManager +{ + public void Initialize(); + + public bool UserHasFeature(NetUserId userId, + ProtoId feature, + bool ifDisabledSponsorhip = true); + + public bool TryGetSponsorOOCColor(NetUserId userId, [NotNullWhen(true)] out Color? color); +} + +public sealed class CP14SponsorRoleUpdate : NetMessage +{ + public override MsgGroups MsgGroup => MsgGroups.Command; + + public ProtoId Role { get; set; } + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + Role = buffer.ReadString(); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + buffer.Write(Role); + } +} diff --git a/Content.Shared/_CP14/Sponsor/CP14SponsorRolePrototype.cs b/Content.Shared/_CP14/Sponsor/CP14SponsorRolePrototype.cs new file mode 100644 index 0000000000..569d12635d --- /dev/null +++ b/Content.Shared/_CP14/Sponsor/CP14SponsorRolePrototype.cs @@ -0,0 +1,33 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._CP14.Sponsor; + +[Prototype("sponsorRole")] +public sealed partial class CP14SponsorRolePrototype : IPrototype +{ + [IdDataField] public string ID { get; } = string.Empty; + + [DataField(required: true)] + public string Name = string.Empty; + + [DataField(required: true)] + public string DiscordRoleId = string.Empty; + + [DataField] + public Color? Color = null; + + [DataField] + public float Priority = 0; + + [DataField] + public bool Examinable = false; +} + +[Prototype("sponsorFeature")] +public sealed partial class CP14SponsorFeaturePrototype : IPrototype +{ + [IdDataField] public string ID { get; } = string.Empty; + + [DataField] + public float MinPriority = 1; +} diff --git a/Resources/ConfigPresets/_CP14/Dev.toml b/Resources/ConfigPresets/_CP14/Dev.toml index 29581af64d..43386391e9 100644 --- a/Resources/ConfigPresets/_CP14/Dev.toml +++ b/Resources/ConfigPresets/_CP14/Dev.toml @@ -67,4 +67,5 @@ mob_pushing = true [cp14] discord_auth_enabled = true -closet_beta_test = true \ No newline at end of file +closet_beta_test = true +sponsor_enabled = true \ No newline at end of file diff --git a/Resources/Locale/en-US/_CP14/job/requirements.ftl b/Resources/Locale/en-US/_CP14/job/requirements.ftl new file mode 100644 index 0000000000..774e7e7325 --- /dev/null +++ b/Resources/Locale/en-US/_CP14/job/requirements.ftl @@ -0,0 +1 @@ +cp14-role-req-sponsor-feature-req = You must have at least the “{$role}” role in the discord server to access this. \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_CP14/job/requirements.ftl b/Resources/Locale/ru-RU/_CP14/job/requirements.ftl new file mode 100644 index 0000000000..e3378abc91 --- /dev/null +++ b/Resources/Locale/ru-RU/_CP14/job/requirements.ftl @@ -0,0 +1 @@ +cp14-role-req-sponsor-feature-req = Вы должны иметь как минимум роль "{$role}" в дискорд сервере, чтобы получить доступ к этому. \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Sponsor/features.yml b/Resources/Prototypes/_CP14/Sponsor/features.yml new file mode 100644 index 0000000000..4a17a92d8d --- /dev/null +++ b/Resources/Prototypes/_CP14/Sponsor/features.yml @@ -0,0 +1,3 @@ +- type: sponsorFeature + id: PriorityJoin # Allows you to connect to the server by bypassing the queue + minPriority: 1 \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Sponsor/roles.yml b/Resources/Prototypes/_CP14/Sponsor/roles.yml new file mode 100644 index 0000000000..e22c36452e --- /dev/null +++ b/Resources/Prototypes/_CP14/Sponsor/roles.yml @@ -0,0 +1,76 @@ +# Development assistance + +- type: sponsorRole + id: Contributor + name: Contributor + discordRoleId: "1224637355193929818" + priority: 1 + color: "#1fbf51" + +- type: sponsorRole + id: Moderator + name: "Discord Moderator" + discordRoleId: "1225071736286875718" + priority: 1 + color: "#1fbf51" + +- type: sponsorRole + id: Maintainer + name: Maintainer + discordRoleId: "1263856724868333688" + priority: 2 + color: "#287fc7" + +- type: sponsorRole + id: Headmin + name: Headmin + discordRoleId: "1359572736095289544" + priority: 3 + color: "#2c65f5" + +- type: sponsorRole + id: LoreKeeper + name: Lore Keeper + discordRoleId: "1264150382821773354" + priority: 3 + color: "#2c65f5" + +- type: sponsorRole + id: HeadMapper + name: Head Mapper + discordRoleId: "1316306180297064488" + priority: 3 + color: "#2c65f5" + +- type: sponsorRole + id: ArtDirector + name: Art Director + discordRoleId: "1242043180745097236" + priority: 3 + color: "#2c65f5" + +# Sponsorship assistance + +- type: sponsorRole + id: Sponsor1 + name: Sponsorship I + discordRoleId: "1254132975897935986" + priority: 1.1 + color: "#bf397c" + examinable: true + +- type: sponsorRole + id: Sponsor2 + name: Sponsorship II + discordRoleId: "1316306142833414195" + priority: 2 + color: "#d92567" + examinable: true + +- type: sponsorRole + id: Sponsor3 + name: Sponsorship III + discordRoleId: "1327377639882752052" + priority: 3 + color: "#fa1639" + examinable: true \ No newline at end of file