From efc682d7bbb86bf199846b72165ed4cc848ca770 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Tue, 28 Oct 2025 15:36:58 -0300 Subject: [PATCH 1/7] feat: begin consent system --- Content.Shared/IoC/SharedContentIoC.cs | 4 +- .../Consent/Components/ConsentComponent.cs | 14 +++ .../EntitySystems/SharedConsentSystem.cs | 21 ++++ .../_DEN/Consent/Events/ConsentEvents.cs | 50 +++++++++ .../Consent/Events/ConsentUpdatedEventArgs.cs | 15 +++ .../_DEN/Consent/Managers/ConsentManager.cs | 105 ++++++++++++++++++ .../_DEN/Consent/Managers/IConsentManager.cs | 22 ++++ .../Prototypes/ConsentTogglePrototype.cs | 17 +++ 8 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 Content.Shared/_DEN/Consent/Components/ConsentComponent.cs create mode 100644 Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs create mode 100644 Content.Shared/_DEN/Consent/Events/ConsentEvents.cs create mode 100644 Content.Shared/_DEN/Consent/Events/ConsentUpdatedEventArgs.cs create mode 100644 Content.Shared/_DEN/Consent/Managers/ConsentManager.cs create mode 100644 Content.Shared/_DEN/Consent/Managers/IConsentManager.cs create mode 100644 Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs 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..b84aa19095e --- /dev/null +++ b/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs @@ -0,0 +1,14 @@ +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 Dictionary, bool> 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..dd9afb82b38 --- /dev/null +++ b/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._DEN.Consent.EntitySystems; + +/// +/// This handles updating consent based on entity events. +/// +public abstract class SharedConsentSystem : EntitySystem +{ + /// + public override void Initialize() + { + base.Initialize(); + } +} + +[Serializable, NetSerializable] +public record struct UserConsentToggle(ProtoId ToggleId); + diff --git a/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs new file mode 100644 index 00000000000..89c87cf37ef --- /dev/null +++ b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs @@ -0,0 +1,50 @@ +using Content.Shared._DEN.Consent.EntitySystems; +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._DEN.Consent.Events; + +/// +/// Used to inform same-side about updated consents, for whatever reason. +/// +/// The of the user with their toggle updated. +/// The containing the toggle id and its new value. +public record struct ConsentUpdated(NetUserId UserId, UserConsentToggle Toggle); + +/// +/// 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 UpdatedConsents = []; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + var length = buffer.ReadVariableInt32(); + + UpdatedConsents.Clear(); + + for (var i = 0; i < length; i++) + { + var updatedConsent = (ProtoId) buffer.ReadString(); + var newValue = buffer.ReadBoolean(); + var newToggle = new UserConsentToggle(updatedConsent, newValue); + UpdatedConsents.Add(newToggle); + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + buffer.WriteVariableInt32(UpdatedConsents.Count); + + foreach (var consentPair in UpdatedConsents) + { + buffer.Write(consentPair.ToggleId); + buffer.Write(consentPair.ToggleValue); + } + } +} 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..5b81e65b3c5 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs @@ -0,0 +1,105 @@ +using System.Linq; +using System.Threading; +using Content.Shared._DEN.Consent.Events; +using Content.Shared._DEN.Consent.Prototypes; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared._DEN.Consent.Managers; + +/// +/// Used to store consent information. +/// Only stores values that are not the default value of the toggle. +/// +public abstract class ConsentManager : IConsentManager +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + [ViewVariables] + protected Dictionary>> InternalConsents { get; } = new(); + protected Dictionary, bool> DefaultToggleValues { get; } = new(); + + protected readonly ReaderWriterLockSlim Lock = new(); + + public Dictionary>> UserConsents + { + get + { + Lock.EnterReadLock(); + try + { + return InternalConsents.ShallowClone(); + } + finally + { + Lock.ExitReadLock(); + } + } + } + + public event Action? OnConsentUpdated; + public event Action? OnConsentSet; + + public void Initialize() + { + _protoManager.PrototypesReloaded += OnPrototypesReloaded; + CacheDefaultToggleValues(); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (args.WasModified()) + CacheDefaultToggleValues(); + } + + private void CacheDefaultToggleValues() + { + DefaultToggleValues.Clear(); + + foreach (var toggle in _protoManager.EnumeratePrototypes()) + { + DefaultToggleValues[toggle] = toggle.DefaultValue; + } + } + + public void SetConsentToggle(NetUserId userId, ProtoId toggle, bool newValue) + { + var toggles = GetConsentToggles(userId); + + if (newValue == DefaultToggleValues[toggle]) + toggles.Remove(toggle); + else + AddConsentToggle(ref toggles, toggle); + + InternalConsents[userId] = toggles; + + var updatedEvent = new ConsentUpdatedEventArgs(userId, toggle, newValue); + OnConsentUpdated?.Invoke(updatedEvent); + } + + public void SetConsentToggles(NetUserId userId, List> toggles) + { + InternalConsents[userId] = toggles; + OnConsentSet?.Invoke(); + } + + public List> GetConsentToggles(NetUserId userId) + { + var exists = UserConsents.TryGetValue(userId, out var consentToggles); + + if (!exists || consentToggles == null) + return []; + + return consentToggles; + } + + private void AddConsentToggle(ref List> toggles, + ProtoId toggle) + { + if (toggles.Contains(toggle)) + return; + + toggles.Add(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..b888c6ce9f8 --- /dev/null +++ b/Content.Shared/_DEN/Consent/Managers/IConsentManager.cs @@ -0,0 +1,22 @@ +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; + protected Dictionary>> UserConsents { get; } + + + void SetConsentToggle(NetUserId userId, ProtoId toggle, bool newValue); + void SetConsentToggles(NetUserId userId, List> toggles); + List> GetConsentToggles(NetUserId userId); +} + diff --git a/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs b/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs new file mode 100644 index 00000000000..2742c6f5b04 --- /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] +public sealed partial class ConsentTogglePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + [DataField] + public bool DefaultValue { get; set; } +} From 4f9c683e59b9b96c3e8495e4715d87cd087310cf Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Wed, 29 Oct 2025 03:36:01 -0300 Subject: [PATCH 2/7] feat: finish backend --- .../Consent/EntitySystems/ConsentSystem.cs | 5 + .../_DEN/Consent/ConsentCommands.cs | 81 ++++++++++++ .../Consent/EntitySystems/ConsentSystem.cs | 68 ++++++++++ Content.Shared/Entry/EntryPoint.cs | 2 + .../Consent/Components/ConsentComponent.cs | 3 +- .../EntitySystems/SharedConsentSystem.cs | 23 ++-- .../_DEN/Consent/Events/ConsentEvents.cs | 18 ++- .../_DEN/Consent/Managers/ConsentManager.cs | 116 ++++++++++++------ .../_DEN/Consent/Managers/IConsentManager.cs | 9 +- .../Prototypes/ConsentTogglePrototype.cs | 2 +- Resources/Prototypes/_DEN/Consent/consent.yml | 6 + 11 files changed, 268 insertions(+), 65 deletions(-) create mode 100644 Content.Client/_DEN/Consent/EntitySystems/ConsentSystem.cs create mode 100644 Content.Server/_DEN/Consent/ConsentCommands.cs create mode 100644 Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs create mode 100644 Resources/Prototypes/_DEN/Consent/consent.yml 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..a42fd480c15 --- /dev/null +++ b/Content.Server/_DEN/Consent/ConsentCommands.cs @@ -0,0 +1,81 @@ +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 string Description { get; } = "Sets a consent ID to a value for yourself."; + public override string Help { get; } = "setconsent consentId newValue"; + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2 || shell.Player == null) + return; + + if (!_protoManager.TryIndex(args[0], out var toggle)) + return; + + if (!bool.TryParse(args[1], out var newValue)) + return; + + _consentManager.SetConsentToggle(shell.Player.UserId, args[0], newValue); + shell.WriteLine($"Set consent `{args[0]}` to `{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 string Description { get; } = "Gets consent value."; + public override string Help { 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); + + shell.WriteLine($"Different consents: \n- {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..cc7aec06df5 --- /dev/null +++ b/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs @@ -0,0 +1,68 @@ +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 Content.Shared.Mind.Components; +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; + + consentComponent.ConsentToggles = consentToggles; + Dirty((attachedEntity, consentComponent)); + } + + 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 db8d6a6abdd..facf840c850 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.IoC; using Content.Shared.Maps; @@ -45,6 +46,7 @@ public override void PostInit() InitTileDefinitions(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); // DEN: Consent system #if DEBUG var configMan = IoCManager.Resolve(); diff --git a/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs b/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs index b84aa19095e..35a75202c93 100644 --- a/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs +++ b/Content.Shared/_DEN/Consent/Components/ConsentComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Consent.Prototypes; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -10,5 +11,5 @@ namespace Content.Shared._DEN.Consent.Components; public sealed partial class ConsentComponent : Component { [DataField, AutoNetworkedField] - public Dictionary, bool> ConsentToggles { get; set; } = new(); + public List> ConsentToggles { get; set; } = new(); } diff --git a/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs b/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs index dd9afb82b38..a19cbb10c8d 100644 --- a/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs +++ b/Content.Shared/_DEN/Consent/EntitySystems/SharedConsentSystem.cs @@ -1,21 +1,24 @@ +using Content.Shared._DEN.Consent.Components; +using Content.Shared._DEN.Consent.Managers; using Content.Shared._DEN.Consent.Prototypes; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; namespace Content.Shared._DEN.Consent.EntitySystems; -/// -/// This handles updating consent based on entity events. -/// public abstract class SharedConsentSystem : EntitySystem { - /// - public override void Initialize() + [Dependency] protected readonly IConsentManager ConsentManager = default!; + + public bool HasConsent(EntityUid uid, ProtoId toggle) { - base.Initialize(); + var defaultValue = ConsentManager.GetDefaultValue(toggle); + + if (TryComp(uid, out var consent) + && consent.ConsentToggles.Contains(toggle)) + return !defaultValue; + + return defaultValue; } } -[Serializable, NetSerializable] -public record struct UserConsentToggle(ProtoId ToggleId); - +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 index 89c87cf37ef..ab712365237 100644 --- a/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs +++ b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs @@ -1,4 +1,5 @@ using Content.Shared._DEN.Consent.EntitySystems; +using Content.Shared._DEN.Consent.Prototypes; using Lidgren.Network; using Robust.Shared.Network; using Robust.Shared.Prototypes; @@ -11,7 +12,7 @@ namespace Content.Shared._DEN.Consent.Events; /// /// The of the user with their toggle updated. /// The containing the toggle id and its new value. -public record struct ConsentUpdated(NetUserId UserId, UserConsentToggle Toggle); +public record struct ConsentUpdated(NetUserId UserId, ProtoId Toggle, bool NewValue); /// /// Used to update consent settings on either the client or the server. @@ -20,31 +21,28 @@ public sealed class MsgUpdateConsent : NetMessage { public override MsgGroups MsgGroup => MsgGroups.Command; - public List UpdatedConsents = []; + public List> NotDefaultConsents = []; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { var length = buffer.ReadVariableInt32(); - UpdatedConsents.Clear(); + NotDefaultConsents.Clear(); for (var i = 0; i < length; i++) { var updatedConsent = (ProtoId) buffer.ReadString(); - var newValue = buffer.ReadBoolean(); - var newToggle = new UserConsentToggle(updatedConsent, newValue); - UpdatedConsents.Add(newToggle); + NotDefaultConsents.Add(updatedConsent); } } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.WriteVariableInt32(UpdatedConsents.Count); + buffer.WriteVariableInt32(NotDefaultConsents.Count); - foreach (var consentPair in UpdatedConsents) + foreach (var consentId in NotDefaultConsents) { - buffer.Write(consentPair.ToggleId); - buffer.Write(consentPair.ToggleValue); + buffer.Write(consentId); } } } diff --git a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs index 5b81e65b3c5..ac299c2d6d1 100644 --- a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs +++ b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs @@ -1,8 +1,10 @@ using System.Linq; using System.Threading; +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; using Robust.Shared.Utility; @@ -12,38 +14,23 @@ namespace Content.Shared._DEN.Consent.Managers; /// Used to store consent information. /// Only stores values that are not the default value of the toggle. /// -public abstract class ConsentManager : IConsentManager +public sealed class ConsentManager : IConsentManager { + + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [ViewVariables] - protected Dictionary>> InternalConsents { get; } = new(); - protected Dictionary, bool> DefaultToggleValues { get; } = new(); - - protected readonly ReaderWriterLockSlim Lock = new(); - - public Dictionary>> UserConsents - { - get - { - Lock.EnterReadLock(); - try - { - return InternalConsents.ShallowClone(); - } - finally - { - Lock.ExitReadLock(); - } - } - } + private Dictionary> InternalConsents { get; } = new(); + private Dictionary, bool> DefaultToggleValues { get; } = new(); public event Action? OnConsentUpdated; - public event Action? OnConsentSet; + public event Action? OnConsentSet; public void Initialize() { _protoManager.PrototypesReloaded += OnPrototypesReloaded; + _playerManager.PlayerStatusChanged += (_, args) => OnPlayerStatusChanged(args); CacheDefaultToggleValues(); } @@ -53,53 +40,104 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) 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] = toggle.DefaultValue; + DefaultToggleValues[toggle.ID] = toggle.DefaultValue; } } - public void SetConsentToggle(NetUserId userId, ProtoId toggle, bool newValue) + public void SetConsentToggle(NetUserId userId, ProtoId toggleId, bool newValue) { - var toggles = GetConsentToggles(userId); + if (!InternalConsents.ContainsKey(userId)) + InternalConsents[userId] = GetDefaultToggles(); + + var toggles = InternalConsents[userId] + .Where(t => t.ToggleId != toggleId) + .ToList(); + + var toggle = new UserConsentInfo(toggleId, newValue); - if (newValue == DefaultToggleValues[toggle]) - toggles.Remove(toggle); - else - AddConsentToggle(ref toggles, toggle); + if (newValue != DefaultToggleValues[toggleId]) + toggles.Add(toggle); InternalConsents[userId] = toggles; - var updatedEvent = new ConsentUpdatedEventArgs(userId, toggle, newValue); + var updatedEvent = new ConsentUpdatedEventArgs(userId, toggle.ToggleId, toggle.ToggleValue); OnConsentUpdated?.Invoke(updatedEvent); } - public void SetConsentToggles(NetUserId userId, List> toggles) + public void SetConsentToggles(NetUserId userId, List toggles) { InternalConsents[userId] = toggles; - OnConsentSet?.Invoke(); + OnConsentSet?.Invoke(userId); } public List> GetConsentToggles(NetUserId userId) { - var exists = UserConsents.TryGetValue(userId, out var consentToggles); + var exists = InternalConsents.TryGetValue(userId, out var consentToggles); if (!exists || consentToggles == null) return []; - return consentToggles; + 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 void AddConsentToggle(ref List> toggles, - ProtoId toggle) + private List GetConsentTogglesExcept(NetUserId userId, ProtoId toggleId) { - if (toggles.Contains(toggle)) - return; + // ReSharper disable once ConvertIfStatementToReturnStatement + if (!InternalConsents.TryGetValue(userId, out var consentToggles)) + return GetDefaultToggles(); - toggles.Add(toggle); + 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 index b888c6ce9f8..4997269a1c0 100644 --- a/Content.Shared/_DEN/Consent/Managers/IConsentManager.cs +++ b/Content.Shared/_DEN/Consent/Managers/IConsentManager.cs @@ -1,3 +1,4 @@ +using Content.Shared._DEN.Consent.EntitySystems; using Content.Shared._DEN.Consent.Events; using Content.Shared._DEN.Consent.Prototypes; using Robust.Shared.Network; @@ -11,12 +12,12 @@ namespace Content.Shared._DEN.Consent.Managers; public interface IConsentManager { event Action? OnConsentUpdated; - event Action? OnConsentSet; - protected Dictionary>> UserConsents { get; } - + event Action? OnConsentSet; + void Initialize(); void SetConsentToggle(NetUserId userId, ProtoId toggle, bool newValue); - void SetConsentToggles(NetUserId userId, List> toggles); + 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 index 2742c6f5b04..973bed27a01 100644 --- a/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs +++ b/Content.Shared/_DEN/Consent/Prototypes/ConsentTogglePrototype.cs @@ -5,7 +5,7 @@ namespace Content.Shared._DEN.Consent.Prototypes; /// /// This is a prototype for declaring consent toggles. /// -[Prototype] +[Prototype("consent")] public sealed partial class ConsentTogglePrototype : IPrototype { /// 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 From 21fefb644e0ab1697d0749742521333cadfccdf1 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Wed, 29 Oct 2025 03:38:08 -0300 Subject: [PATCH 3/7] chore: use 0-count message --- Content.Server/_DEN/Consent/ConsentCommands.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Content.Server/_DEN/Consent/ConsentCommands.cs b/Content.Server/_DEN/Consent/ConsentCommands.cs index a42fd480c15..a5c9f13cab0 100644 --- a/Content.Server/_DEN/Consent/ConsentCommands.cs +++ b/Content.Server/_DEN/Consent/ConsentCommands.cs @@ -65,6 +65,12 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var consents = _consentManager.GetConsentToggles(shell.Player.UserId); var consentsMessage = string.Join("\n -", consents); + if (consents.Count == 0) + { + shell.WriteLine("No different consents; all are using default."); + return; + } + shell.WriteLine($"Different consents: \n- {consentsMessage}"); } From b50a068de4f078ef13e315a4f2cf7847b7d4a941 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Wed, 29 Oct 2025 04:04:39 -0300 Subject: [PATCH 4/7] chore: use existing methods --- Content.Shared/_DEN/Consent/Managers/ConsentManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs index ac299c2d6d1..23160fe43e7 100644 --- a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs +++ b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs @@ -63,10 +63,7 @@ public void SetConsentToggle(NetUserId userId, ProtoId t if (!InternalConsents.ContainsKey(userId)) InternalConsents[userId] = GetDefaultToggles(); - var toggles = InternalConsents[userId] - .Where(t => t.ToggleId != toggleId) - .ToList(); - + var toggles = GetConsentTogglesExcept(userId, toggleId); var toggle = new UserConsentInfo(toggleId, newValue); if (newValue != DefaultToggleValues[toggleId]) From 1fc2546a48a4467a0de4e2c13f98b61577d4fb04 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Wed, 29 Oct 2025 04:05:59 -0300 Subject: [PATCH 5/7] fix: don't return a list that includes the toggle --- Content.Shared/_DEN/Consent/Managers/ConsentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs index 23160fe43e7..daddfb31927 100644 --- a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs +++ b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs @@ -118,7 +118,7 @@ private List GetConsentTogglesExcept(NetUserId userId, ProtoId< { // ReSharper disable once ConvertIfStatementToReturnStatement if (!InternalConsents.TryGetValue(userId, out var consentToggles)) - return GetDefaultToggles(); + consentToggles = GetDefaultToggles(); var consentIds = new List(); From 031d4627425375802198d4ad144c74cffc98d3e2 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Wed, 29 Oct 2025 04:39:44 -0300 Subject: [PATCH 6/7] chore: cleanup and docs --- .../_DEN/Consent/EntitySystems/ConsentSystem.cs | 6 ++++-- .../_DEN/Consent/Events/ConsentEvents.cs | 16 +++++++++++----- .../_DEN/Consent/Managers/ConsentManager.cs | 4 ---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs b/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs index cc7aec06df5..c23c5291d98 100644 --- a/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs +++ b/Content.Server/_DEN/Consent/EntitySystems/ConsentSystem.cs @@ -2,7 +2,6 @@ using Content.Shared._DEN.Consent.EntitySystems; using Content.Shared._DEN.Consent.Events; using Content.Shared._DEN.Consent.Managers; -using Content.Shared.Mind.Components; using Robust.Server.Player; using Robust.Shared.Network; using Robust.Shared.Player; @@ -41,8 +40,11 @@ private void OnConsentUpdated(NetUserId userId) if (consentComponent.ConsentToggles == consentToggles) return; + var ent = (attachedEntity, consentComponent); consentComponent.ConsentToggles = consentToggles; - Dirty((attachedEntity, consentComponent)); + + Dirty(ent); + RaiseLocalEvent(new ConsentUpdatedEvent(attachedEntity)); } private void OnMindAdded(PlayerAttachedEvent ev) diff --git a/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs index ab712365237..8ae79701cce 100644 --- a/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs +++ b/Content.Shared/_DEN/Consent/Events/ConsentEvents.cs @@ -1,18 +1,24 @@ -using Content.Shared._DEN.Consent.EntitySystems; +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 about updated consents, for whatever reason. +/// Used to inform same-side entity systems about updated consents, for whatever reason. /// -/// The of the user with their toggle updated. -/// The containing the toggle id and its new value. -public record struct ConsentUpdated(NetUserId UserId, ProtoId Toggle, bool NewValue); +/// +/// 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. diff --git a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs index daddfb31927..1a5c1c5a032 100644 --- a/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs +++ b/Content.Shared/_DEN/Consent/Managers/ConsentManager.cs @@ -1,12 +1,9 @@ -using System.Linq; -using System.Threading; 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; -using Robust.Shared.Utility; namespace Content.Shared._DEN.Consent.Managers; @@ -16,7 +13,6 @@ namespace Content.Shared._DEN.Consent.Managers; /// public sealed class ConsentManager : IConsentManager { - [Dependency] private readonly ISharedPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; From c6e3546075c2de1fa2964cabdf90f36a779cb7cd Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Thu, 12 Feb 2026 10:58:35 -0400 Subject: [PATCH 7/7] chore: use localization --- .../_DEN/Consent/ConsentCommands.cs | 22 ++++++++++++------- .../Locale/en-US/_DEN/commands/consent.ftl | 14 ++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 Resources/Locale/en-US/_DEN/commands/consent.ftl diff --git a/Content.Server/_DEN/Consent/ConsentCommands.cs b/Content.Server/_DEN/Consent/ConsentCommands.cs index a5c9f13cab0..2c957c304f3 100644 --- a/Content.Server/_DEN/Consent/ConsentCommands.cs +++ b/Content.Server/_DEN/Consent/ConsentCommands.cs @@ -13,21 +13,29 @@ public sealed class SetConsentCommand : LocalizedCommands [Dependency] private readonly IPrototypeManager _protoManager = default!; public override string Command { get; } = "setconsent"; - public override string Description { get; } = "Sets a consent ID to a value for yourself."; - public override string Help { get; } = "setconsent consentId newValue"; + 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 var toggle)) + 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($"Set consent `{args[0]}` to `{args[1]}`"); + shell.WriteLine(Loc.GetString("cmd-setconsent-success", ("consentId", args[0]), ("value", args[1]))); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) @@ -54,8 +62,6 @@ public sealed class ConsentCommand : LocalizedCommands [Dependency] private readonly IConsentManager _consentManager = default!; public override string Command { get; } = "consents"; - public override string Description { get; } = "Gets consent value."; - public override string Help { get; } = "consents"; public override void Execute(IConsoleShell shell, string argStr, string[] args) { @@ -67,11 +73,11 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (consents.Count == 0) { - shell.WriteLine("No different consents; all are using default."); + shell.WriteLine(Loc.GetString("cmd-consent-no-different")); return; } - shell.WriteLine($"Different consents: \n- {consentsMessage}"); + shell.WriteLine(Loc.GetString("cmd-consent-differences", ("differentConsents", consentsMessage))); } public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) 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