diff --git a/Content.Client/_DEN/Consent/EntitySystems/ConsentSystem.cs b/Content.Client/_DEN/Consent/EntitySystems/ConsentSystem.cs new file mode 100644 index 00000000000..ff004950104 --- /dev/null +++ b/Content.Client/_DEN/Consent/EntitySystems/ConsentSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared._DEN.Consent.EntitySystems; + +namespace Content.Client._DEN.Consent.EntitySystems; + +public sealed class ConsentSystem : SharedConsentSystem; diff --git a/Content.Server/_DEN/Consent/ConsentCommands.cs b/Content.Server/_DEN/Consent/ConsentCommands.cs new file mode 100644 index 00000000000..2c957c304f3 --- /dev/null +++ b/Content.Server/_DEN/Consent/ConsentCommands.cs @@ -0,0 +1,93 @@ +using Content.Shared._DEN.Consent.Managers; +using Content.Shared._DEN.Consent.Prototypes; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Prototypes; + +namespace Content.Server._DEN.Consent; + +[AnyCommand] +public sealed class SetConsentCommand : LocalizedCommands +{ + [Dependency] private readonly IConsentManager _consentManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + public override string Command { get; } = "setconsent"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2 || shell.Player == null) + { + shell.WriteError(Loc.GetString("cmd-setconsent-error-args", ("usage", Help))); + return; + } + + if (!_protoManager.TryIndex(args[0], out _)) + { + shell.WriteError(Loc.GetString("cmd-setconsent-error-invalid-consent", ("consentId", args[0]), ("usage", Help))); + return; + } + + if (!bool.TryParse(args[1], out var newValue)) + { + shell.WriteError(Loc.GetString("cmd-setconsent-error-bool", ("value", args[1]), ("usage", Help))); + return; + } + + _consentManager.SetConsentToggle(shell.Player.UserId, args[0], newValue); + shell.WriteLine(Loc.GetString("cmd-setconsent-success", ("consentId", args[0]), ("value", args[1]))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + var options = CompletionHelper.PrototypeIDs(); + return CompletionResult.FromOptions(options); + } + + if (args.Length == 2) + { + var options = CompletionHelper.Booleans; + return CompletionResult.FromOptions(options); + } + + return CompletionResult.Empty; + } +} + +[AnyCommand] +public sealed class ConsentCommand : LocalizedCommands +{ + [Dependency] private readonly IConsentManager _consentManager = default!; + + public override string Command { get; } = "consents"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 1 || shell.Player == null) + return; + + var consents = _consentManager.GetConsentToggles(shell.Player.UserId); + var consentsMessage = string.Join("\n -", consents); + + if (consents.Count == 0) + { + shell.WriteLine(Loc.GetString("cmd-consent-no-different")); + return; + } + + shell.WriteLine(Loc.GetString("cmd-consent-differences", ("differentConsents", consentsMessage))); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + var options = CompletionHelper.PrototypeIDs(); + return CompletionResult.FromOptions(options); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs b/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs new file mode 100644 index 00000000000..c23c5291d98 --- /dev/null +++ b/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs @@ -0,0 +1,70 @@ +using Content.Shared._DEN.Consent.Components; +using Content.Shared._DEN.Consent.EntitySystems; +using Content.Shared._DEN.Consent.Events; +using Content.Shared._DEN.Consent.Managers; +using Robust.Server.Player; +using Robust.Shared.Network; +using Robust.Shared.Player; + +namespace Content.Server._DEN.Consent.EntitySystems; + +/// +/// This handles server-sided consent information. +/// +public sealed class ConsentSystem : SharedConsentSystem +{ + [Dependency] private readonly IConsentManager _consentManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnMindRemoved); + + _consentManager.OnConsentUpdated += args => OnConsentUpdated(args.UserId); + _consentManager.OnConsentSet += OnConsentUpdated; + } + + private void OnConsentUpdated(NetUserId userId) + { + if (!_playerManager.TryGetSessionById(userId, out var session) + || session.AttachedEntity is not { Valid: true } attachedEntity) + return; + + var consentToggles = ConsentManager.GetConsentToggles(userId); + var consentComponent = EnsureComp(attachedEntity); + + if (consentComponent.ConsentToggles == consentToggles) + return; + + var ent = (attachedEntity, consentComponent); + consentComponent.ConsentToggles = consentToggles; + + Dirty(ent); + RaiseLocalEvent(new ConsentUpdatedEvent(attachedEntity)); + } + + private void OnMindAdded(PlayerAttachedEvent ev) + { + BuildConsentComponent(ev); + } + + private void OnMindRemoved(PlayerDetachedEvent ev) + { + RemComp(ev.Entity); + } + + private void BuildConsentComponent(PlayerAttachedEvent ev) + { + var userId = ev.Player.UserId; + + var consentToggles = ConsentManager.GetConsentToggles(userId); + var consentComponent = EnsureComp(ev.Entity); + consentComponent.ConsentToggles = consentToggles; + + Dirty((ev.Entity, consentComponent)); + } +} diff --git a/Content.Shared/Entry/EntryPoint.cs b/Content.Shared/Entry/EntryPoint.cs index 1b5755dd666..55b302e8e3c 100644 --- a/Content.Shared/Entry/EntryPoint.cs +++ b/Content.Shared/Entry/EntryPoint.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Content.Shared._DEN.Consent.Managers; using Content.Shared.Humanoid.Markings; using Content.Shared.Maps; using Robust.Shared; @@ -47,6 +48,7 @@ public override void PostInit() InitTileDefinitions(); Dependencies.Resolve().Initialize(); + Dependencies.Resolve().Initialize(); // DEN: Consent system #if DEBUG _configurationManager.OverrideDefault(CVars.NetFakeLagMin, 0.075f); diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs index f23b9f83558..060bcc9383d 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.Consent.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: Consent System } } } diff --git a/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs b/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs new file mode 100644 index 00000000000..35a75202c93 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Consent.Components; + +/// +/// This is used for sharing a user's consent information with other clients. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ConsentComponent : Component +{ + [DataField, AutoNetworkedField] + public List> ConsentToggles { get; set; } = new(); +} diff --git a/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs b/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs new file mode 100644 index 00000000000..a19cbb10c8d --- /dev/null +++ b/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs @@ -0,0 +1,24 @@ +using Content.Shared._DEN.Consent.Components; +using Content.Shared._DEN.Consent.Managers; +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Consent.EntitySystems; + +public abstract class SharedConsentSystem : EntitySystem +{ + [Dependency] protected readonly IConsentManager ConsentManager = default!; + + public bool HasConsent(EntityUid uid, ProtoId toggle) + { + var defaultValue = ConsentManager.GetDefaultValue(toggle); + + if (TryComp(uid, out var consent) + && consent.ConsentToggles.Contains(toggle)) + return !defaultValue; + + return defaultValue; + } +} + +public record struct UserConsentInfo(ProtoId ToggleId, bool ToggleValue); diff --git a/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs new file mode 100644 index 00000000000..8ae79701cce --- /dev/null +++ b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs @@ -0,0 +1,54 @@ +using Content.Shared._DEN.Consent.Components; +using Content.Shared._DEN.Consent.Prototypes; +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Content.Shared._DEN.Consent.Managers; + +namespace Content.Shared._DEN.Consent.Events; + +/// +/// Used to inform same-side entity systems about updated consents, for whatever reason. +/// +/// +/// This will only run if the user in question has a valid AttachedEntity. +/// contains an action that always runs on update. +/// +public sealed class ConsentUpdatedEvent(EntityUid uid) : EntityEventArgs +{ + public EntityUid Entity { get; } = uid; +} + +/// +/// Used to update consent settings on either the client or the server. +/// +public sealed class MsgUpdateConsent : NetMessage +{ + public override MsgGroups MsgGroup => MsgGroups.Command; + + public List> NotDefaultConsents = []; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + var length = buffer.ReadVariableInt32(); + + NotDefaultConsents.Clear(); + + for (var i = 0; i < length; i++) + { + var updatedConsent = (ProtoId) buffer.ReadString(); + NotDefaultConsents.Add(updatedConsent); + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + buffer.WriteVariableInt32(NotDefaultConsents.Count); + + foreach (var consentId in NotDefaultConsents) + { + buffer.Write(consentId); + } + } +} diff --git a/Content.Shared/_DEN/Consent/Events/ConsentUpdatedEventArgs.cs b/Content.Shared/_DEN/Consent/Events/ConsentUpdatedEventArgs.cs new file mode 100644 index 00000000000..9a97946ff40 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Events/ConsentUpdatedEventArgs.cs @@ -0,0 +1,15 @@ +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Consent.Events; + +public sealed class ConsentUpdatedEventArgs( + NetUserId userId, + ProtoId toggleId, + bool newValue) : EventArgs +{ + public readonly NetUserId UserId = userId; + public readonly ProtoId ToggleId = toggleId; + public readonly bool ToggleValue = newValue; +} diff --git a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs new file mode 100644 index 00000000000..1a5c1c5a032 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs @@ -0,0 +1,136 @@ +using Content.Shared._DEN.Consent.EntitySystems; +using Content.Shared._DEN.Consent.Events; +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Consent.Managers; + +/// +/// Used to store consent information. +/// Only stores values that are not the default value of the toggle. +/// +public sealed class ConsentManager : IConsentManager +{ + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + [ViewVariables] + private Dictionary> InternalConsents { get; } = new(); + private Dictionary, bool> DefaultToggleValues { get; } = new(); + + public event Action? OnConsentUpdated; + public event Action? OnConsentSet; + + public void Initialize() + { + _protoManager.PrototypesReloaded += OnPrototypesReloaded; + _playerManager.PlayerStatusChanged += (_, args) => OnPlayerStatusChanged(args); + CacheDefaultToggleValues(); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (args.WasModified()) + CacheDefaultToggleValues(); + } + + private void OnPlayerStatusChanged(SessionStatusEventArgs args) + { + var toggles = GetDefaultToggles(); + + if (!InternalConsents.TryGetValue(args.Session.UserId, out _)) + InternalConsents[args.Session.UserId] = toggles; + } + + private void CacheDefaultToggleValues() + { + DefaultToggleValues.Clear(); + + foreach (var toggle in _protoManager.EnumeratePrototypes()) + { + DefaultToggleValues[toggle.ID] = toggle.DefaultValue; + } + } + + public void SetConsentToggle(NetUserId userId, ProtoId toggleId, bool newValue) + { + if (!InternalConsents.ContainsKey(userId)) + InternalConsents[userId] = GetDefaultToggles(); + + var toggles = GetConsentTogglesExcept(userId, toggleId); + var toggle = new UserConsentInfo(toggleId, newValue); + + if (newValue != DefaultToggleValues[toggleId]) + toggles.Add(toggle); + + InternalConsents[userId] = toggles; + + var updatedEvent = new ConsentUpdatedEventArgs(userId, toggle.ToggleId, toggle.ToggleValue); + OnConsentUpdated?.Invoke(updatedEvent); + } + + public void SetConsentToggles(NetUserId userId, List toggles) + { + InternalConsents[userId] = toggles; + OnConsentSet?.Invoke(userId); + } + + public List> GetConsentToggles(NetUserId userId) + { + var exists = InternalConsents.TryGetValue(userId, out var consentToggles); + + if (!exists || consentToggles == null) + return []; + + var consentIds = new List>(); + + foreach (var toggle in consentToggles) + { + if (toggle.ToggleValue == DefaultToggleValues[toggle.ToggleId]) + continue; + + consentIds.Add(toggle.ToggleId); + } + + return consentIds; + } + + private List GetDefaultToggles() + { + var defaultToggles = new List(); + + foreach (var pair in DefaultToggleValues) + { + var userConsent = new UserConsentInfo(pair.Key, pair.Value); + defaultToggles.Add(userConsent); + } + + return defaultToggles; + } + + private List GetConsentTogglesExcept(NetUserId userId, ProtoId toggleId) + { + // ReSharper disable once ConvertIfStatementToReturnStatement + if (!InternalConsents.TryGetValue(userId, out var consentToggles)) + consentToggles = GetDefaultToggles(); + + var consentIds = new List(); + + foreach (var toggle in consentToggles) + { + if (toggle.ToggleId == toggleId) + continue; + + consentIds.Add(toggle); + } + + return consentIds; + } + + public bool GetDefaultValue(ProtoId toggle) + { + return DefaultToggleValues[toggle]; + } +} diff --git a/Content.Shared/_DEN/Consent/Managers/IConsentManager.cs b/Content.Shared/_DEN/Consent/Managers/IConsentManager.cs new file mode 100644 index 00000000000..4997269a1c0 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Managers/IConsentManager.cs @@ -0,0 +1,23 @@ +using Content.Shared._DEN.Consent.EntitySystems; +using Content.Shared._DEN.Consent.Events; +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Consent.Managers; + +/// +/// Handle player consent information +/// +public interface IConsentManager +{ + event Action? OnConsentUpdated; + event Action? OnConsentSet; + + void Initialize(); + void SetConsentToggle(NetUserId userId, ProtoId toggle, bool newValue); + void SetConsentToggles(NetUserId userId, List toggles); + List> GetConsentToggles(NetUserId userId); + bool GetDefaultValue(ProtoId toggle); +} + diff --git a/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs b/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs new file mode 100644 index 00000000000..973bed27a01 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._DEN.Consent.Prototypes; + +/// +/// This is a prototype for declaring consent toggles. +/// +[Prototype("consent")] +public sealed partial class ConsentTogglePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + [DataField] + public bool DefaultValue { get; set; } +} diff --git a/Resources/Locale/en-US/_DEN/commands/consent.ftl b/Resources/Locale/en-US/_DEN/commands/consent.ftl new file mode 100644 index 00000000000..40a338f4d79 --- /dev/null +++ b/Resources/Locale/en-US/_DEN/commands/consent.ftl @@ -0,0 +1,14 @@ +cmd-setconsent-desc = Sets a consent ID to a value for yourself. +cmd-setconsent-help = {$command} consentId newValue + +cmd-setconsent-error-args = Not enough arguments. Usage: {$usage} +cmd-setconsent-error-invalid-consent = {$consentId} is not a ConsentTogglePrototype. Usage: {$usage} +cmd-setconsent-error-bool = {$value} is not a valid boolean. Usage: {$usage} + +cmd-setconsent-success = {$consentId} has successfully been set to {$value}. + +cmd-consent-desc = Gets any consents that are different to the default value on a player. +cmd-consent-help = {$command} + +cmd-consent-no-different = No different consents; all consents are using their defaults. +cmd-consent-differences = Different consents:\n- {$differentConsents} \ No newline at end of file diff --git a/Resources/Prototypes/_DEN/Consent/consent.yml b/Resources/Prototypes/_DEN/Consent/consent.yml new file mode 100644 index 00000000000..956ac5831bc --- /dev/null +++ b/Resources/Prototypes/_DEN/Consent/consent.yml @@ -0,0 +1,6 @@ +- type: consent + id: Meow + defaultValue: true + +- type: consent + id: Mew \ No newline at end of file