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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Content.Client/_Floof/Examine/CustomExamineSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public override void Initialize()

private void OnGetVerbs(GetVerbsEvent<Verb> args)
{
if (_player.LocalSession is null || !CanChangeExamine(_player.LocalSession, args.Target))
if (_player.LocalSession is null || !CanChangeExamine(_player.LocalSession, args.Target, out _))
return;

var target = args.Target;
Expand Down Expand Up @@ -88,7 +88,7 @@ private void OpenUi(EntityUid target)
SubtleData = data.subtleData,
Target = GetNetEntity(target)
};
RaiseNetworkEvent(ev);
RaisePredictiveEvent(ev);
};
}

Expand Down
26 changes: 2 additions & 24 deletions Content.Server/_Floof/Examine/CustomExamineSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,5 @@

namespace Content.Server._Floof.Examine;


public sealed class CustomExamineSystem : SharedCustomExamineSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<SetCustomExamineMessage>(OnSetCustomExamineMessage);
}

private void OnSetCustomExamineMessage(SetCustomExamineMessage msg, EntitySessionEventArgs args)
{
var target = GetEntity(msg.Target);
if (!CanChangeExamine(args.SenderSession, target))
return;

var comp = EnsureComp<CustomExamineComponent>(target);

TrimData(ref msg.PublicData, ref msg.SubtleData);
comp.PublicData = msg.PublicData;
comp.SubtleData = msg.SubtleData;

Dirty(target, comp);
}
}
/// Abstract class implementation. This used to contain the SetData logic, but now it's predicted.
public sealed class CustomExamineSystem : SharedCustomExamineSystem { }
2 changes: 1 addition & 1 deletion Content.Shared/_Common/Consent/SharedConsentSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public override void Initialize()

public bool HasConsent(Entity<ConsentComponent?> ent, ProtoId<ConsentTogglePrototype> consentId)
{
if (!Resolve(ent, ref ent.Comp))
if (!Resolve(ent, ref ent.Comp, false)) // Don't log an error
{
// Entities that have never been controlled by a player consent to all mechanics.
return true;
Expand Down
14 changes: 13 additions & 1 deletion Content.Shared/_Floof/Examine/CustomExamineComponent.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;

Expand Down Expand Up @@ -34,7 +35,7 @@ public sealed partial class CustomExamineComponent : Component
}

[DataDefinition, Serializable, NetSerializable]
public partial struct CustomExamineData
public partial struct CustomExamineData : IEquatable<CustomExamineData>
{
[DataField]
public string? Content;
Expand All @@ -59,4 +60,15 @@ public partial struct CustomExamineData
/// </summary>
[DataField]
public TimeSpan LastUpdate;

// God bless Rider for generating this
public bool Equals(CustomExamineData other) =>
Content == other.Content
&& VisibilityRange == other.VisibilityRange
&& ExpireTime.Equals(other.ExpireTime)
&& RequiresConsent == other.RequiresConsent
&& LastUpdate.Equals(other.LastUpdate);

public override bool Equals(object? obj) => obj is CustomExamineData other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Content, VisibilityRange, ExpireTime, RequiresConsent, LastUpdate);
}
176 changes: 169 additions & 7 deletions Content.Shared/_Floof/Examine/CustomExamineSystem.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System.Linq;
using System.Text.RegularExpressions;
using Content.Server._Floof.Examine;
using Content.Shared._Common.Consent;
using Content.Shared._Floof.Util;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Ghost;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
Expand All @@ -18,10 +22,16 @@ namespace Content.Shared._Floof.Examine;
public abstract class SharedCustomExamineSystem : EntitySystem
{
public static ProtoId<ConsentTogglePrototype> NsfwDescConsent = "NSFWDescriptions";
public static ProtoId<ConsentTogglePrototype> CustomExamineChangedByOthersConsent = "CustomExamineChangedByOthers";
public static int PublicMaxLength = 256, SubtleMaxLength = 256;
/// <summary>Max length of any content field, INCLUDING markup.</summary>
public static int AbsolutelyMaxLength = 1024;

/// <summary>The time it takes to update the custom examine of another entity.</summary>
public static TimeSpan SlowCustomExamineChangeDuration = TimeSpan.FromSeconds(3);
/// <summary>The time multiplier for changing examine of another player.</summary>
public static float SlowCustomExaminePlayerPenalty = 2;

private static readonly string[] AllowedTags = // This sucks, shared markup when
[
"bolditalic",
Expand All @@ -35,15 +45,22 @@ public abstract class SharedCustomExamineSystem : EntitySystem
"language",
];

[Dependency] private readonly SharedConsentSystem _consent = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly ISharedAdminManager _admin = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedConsentSystem _consent = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedInteractionSystem _interactions = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfters = default!;
[Dependency] private readonly SharedPopupSystem _popups = default!;

public override void Initialize()
{
SubscribeLocalEvent<CustomExamineComponent, ExaminedEvent>(OnExamined);

SubscribeAllEvent<SetCustomExamineMessage>(OnSetCustomExamineMessage);
SubscribeLocalEvent<SetCustomExamineDoAfterEvent>(OnSetExamineDoAfter);
}

private void OnExamined(Entity<CustomExamineComponent> ent, ref ExaminedEvent args)
Expand Down Expand Up @@ -80,12 +97,99 @@ private void OnExamined(Entity<CustomExamineComponent> ent, ref ExaminedEvent ar
}
}

protected bool CanChangeExamine(ICommonSession actor, EntityUid examinee)
private void OnSetCustomExamineMessage(SetCustomExamineMessage msg, EntitySessionEventArgs args)
{
var target = GetEntity(msg.Target);

// If custom examine data is the same as previous, don't bother
if (TryComp<CustomExamineComponent>(target, out var oldExamine)
&& oldExamine.PublicData.Equals(msg.PublicData)
&& oldExamine.SubtleData.Equals(msg.SubtleData)
)
return;

if (CanInstantlyChangeExamine(args.SenderSession, target, out var reasonLoc))
{
SetData(msg.PublicData, msg.SubtleData, target);
return;
}

if (CanSlowlyChangeExamine(args.SenderSession, target, out reasonLoc))
{
var user = args.SenderSession.AttachedEntity!.Value; // CanSlowlyChangeExamine ensures its not null
if (TryStartExamineChangeDoAfter(msg.PublicData, msg.SubtleData, user, target))
return;
}

// Show a popup to the user if it fails. I wanted to use a chat message here, but it feels kinda wack
// On the other hand popups can easily be obscured by the custom examine window.
if (_net.IsServer && reasonLoc is not null)
_popups.PopupEntity(Loc.GetString(reasonLoc), target, args.SenderSession, PopupType.Medium);
}

private void OnSetExamineDoAfter(SetCustomExamineDoAfterEvent args)
{
return actor.AttachedEntity == examinee && _actionBlocker.CanConsciouslyPerformAction(examinee)
|| _admin.IsAdmin(actor);
if (args.Cancelled || args.Handled || args.Target is not {} target)
return;

// Sanity check
if (!CanSlowlyChangeExamine(args.User, target, out _))
return;

SetData(args.PublicData, args.SubtleData, target);
// Small popup to let other players know what happened
_popups.PopupPredicted(Loc.GetString("custom-examine-data-changed-visibly"), target, null);
}

/// <summary>
/// Returns true if the player can instantly change custom examine.
/// </summary>
protected bool CanInstantlyChangeExamine(ICommonSession actor, EntityUid examinee, out string? reasonLoc)
{
if (actor.AttachedEntity == examinee && _actionBlocker.CanConsciouslyPerformAction(examinee)
|| _admin.IsAdmin(actor) && HasComp<GhostComponent>(actor.AttachedEntity)) // Must be an aghost, not just an adminned player
{
reasonLoc = null;
return true;
}

reasonLoc = "custom-examine-cant-change-data-generic";
return false;
}

protected bool CanSlowlyChangeExamine(ICommonSession actor, EntityUid examinee, out string? reasonLoc)
{
reasonLoc = null;
return actor.AttachedEntity is { } user && CanSlowlyChangeExamine(user, examinee, out reasonLoc);
}

private bool CanSlowlyChangeExamine(EntityUid user, EntityUid examinee, out string? reasonLoc)
{
if (!_actionBlocker.CanInteract(user, examinee)
|| HasComp<GhostComponent>(user) // This sucks, but ghosts actually CAN interact with anything
|| !_interactions.InRangeAndAccessible(user, examinee))
{
reasonLoc = "custom-examine-cant-interact";
return false;
}

// This assumes user != target, prevent the menu from showing up if the target hasn't consented to it
if (HasComp<ActorComponent>(examinee) && !_consent.HasConsent(examinee, CustomExamineChangedByOthersConsent))
{
reasonLoc = "custom-examine-cant-change-data-consent";
return false;
}

reasonLoc = null;
return true;
}

/// <summary>
/// Returns true if the player can change examine at all.
/// </summary>
protected bool CanChangeExamine(ICommonSession actor, EntityUid examinee, out string? reasonLoc) =>
CanInstantlyChangeExamine(actor, examinee, out reasonLoc) || CanSlowlyChangeExamine(actor, examinee, out reasonLoc);

private void CheckExpirations(Entity<CustomExamineComponent> ent)
{
bool Check(ref CustomExamineData data)
Expand Down Expand Up @@ -129,6 +233,64 @@ protected void TrimData(ref CustomExamineData data)
data.Content = null;
}

/// <summary>
/// Sets custom examine data on the entity and dirties it. This performs NO checks.
/// </summary>
private void SetData(CustomExamineData publicData, CustomExamineData subtleData, EntityUid target)
{
var comp = EnsureComp<CustomExamineComponent>(target);

TrimData(ref publicData, ref subtleData);
comp.PublicData = publicData;
comp.SubtleData = subtleData;

Dirty(target, comp);
}

/// <summary>
/// Tries to start a do-after that would change the custom examine of another player. Returns true if the do-after has started or has already been going.
/// This will perform some consent checks.
/// </summary>
public bool TryStartExamineChangeDoAfter(CustomExamineData publicData, CustomExamineData subtleData, EntityUid user, EntityUid target, bool quiet = false)
{
// Basic consent check is already done in CanChangeExamine
// Sanitize message data - remove NSFW contents if the target didn't consent for it
if (!_consent.HasConsent(target, NsfwDescConsent))
{
if (publicData.RequiresConsent)
publicData.Content = "";
if (subtleData.RequiresConsent)
subtleData.Content = "";
}

// If it's a player, change the do-after length respectively and show a popup for them
var delay = SlowCustomExamineChangeDuration;
if (HasComp<ActorComponent>(target))
{
delay *= SlowCustomExaminePlayerPenalty;
if (_net.IsServer && !quiet) // The target will never predict it
_popups.PopupEntity(Loc.GetString("custom-examine-do-after-started-target", ("user", user)), target, target, PopupType.SmallCaution);
}

var doAfterArgs = new DoAfterArgs
{
DuplicateCondition = DuplicateConditions.SameEvent,
CancelDuplicate = true,
BreakOnDamage = true,
BreakOnMove = true,
BreakOnWeightlessMove = true,
NeedHand = false,
RequireCanInteract = true,
Target = target,
User = user,
Delay = delay,
Event = new SetCustomExamineDoAfterEvent(publicData, subtleData),
Broadcast = true, // No component to listen on
};

return _doAfters.TryStartDoAfter(doAfterArgs);
}

protected int LengthWithoutMarkup(string text) => FormattedMessage.RemoveMarkupPermissive(text).Length;

protected int MarkupLength(string text) => text.Length - LengthWithoutMarkup(text);
Expand Down
24 changes: 24 additions & 0 deletions Content.Shared/_Floof/Examine/SetCustomExamineDoAfterEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Content.Shared._Floof.Examine;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;

namespace Content.Server._Floof.Examine;

/// <summary>
/// Do-after event for changing examine data on a different entity.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class SetCustomExamineDoAfterEvent : DoAfterEvent
{
[DataField]
public CustomExamineData PublicData, SubtleData;

public SetCustomExamineDoAfterEvent(CustomExamineData publicData, CustomExamineData subtleData)
{
PublicData = publicData;
SubtleData = subtleData;
}

// Why are we doing this again? All upstream Clone() implementations just return `this`, and it doesn't seem to need cloning in the first place
public override DoAfterEvent Clone() => new SetCustomExamineDoAfterEvent(PublicData, SubtleData);
}
15 changes: 12 additions & 3 deletions Resources/Locale/en-US/_Floof/examine/custom-examine.ftl
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
custom-exam-guidelines-heading = Guidelines
custom-exam-guidelines =
{"["}color=yellow]0.[/color] This menu is scrollable.
{"["}color=yellow]1.[/color] Be [color=#8020ff]concise[/color]. Not everyone will have time to read long paragraphs. Try not to put static character descriptions here.
{"["}color=yellow]2.[/color] When including [color=red]ERP/NSFW content[/color], enable the [color=8020ff]NSFW toggle[/color]. Only people consenting to it will see it.
{"["}color=yellow]3.[/color] Stay [color=8020ff]in-character[/color]. Include things other character would [color=8020ff]see[/color] or otherwise [color=8020ff]perceive[/color] about your character. Don't include subjective info, for example "they look good".
{"["}color=yellow]4.[/color] If necessary, you can use same markup tags in your text as on paper (for example, "\[color=red\]red text\[color\]" to create [color=red]red text[/color]). However, you cannot use headings (because they increase font size drastically). Markup tags won't count towards the character limit.
{"["}color=yellow]2.[/color] When including [color=red]ERP/NSFW content[/color], enable the [color=8020ff]NSFW toggle[/color]. Only people consenting to it will see it.
{"["}color=yellow]3.[/color] Stay [color=8020ff]in-character[/color]. Include things other character would [color=8020ff]see[/color] or otherwise [color=8020ff]perceive[/color] about your character. Don't include subjective info, for example "they look good".
{"["}color=yellow]4.[/color] If necessary, you can use same markup tags in your text as on paper (for example, "\[color=red\]red text\[color\]" to create [color=red]red text[/color]). However, you cannot use headings (because they increase font size drastically). Markup tags won't count towards the character limit.
{"["}color=yellow]5.[/color] You can modify the custom examine of any other entity you can interact with. This will involve a slight do-after. If the object whose examine you're trying to interact is a player, then you can only change it if they have consented to that in their consent settings.
custom-exam-part-title-nsfw = NSFW (require consent)
custom-exam-title-distance = Visible at distance:
custom-exam-title-expiration = Clear after (minutes):
Expand All @@ -22,3 +24,10 @@ custom-examine-too-long = (too long, may lose data)

custom-examine-verb = Custom Examine
custom-examine-nsfw-hidden = [color=#9999a9]<More is hidden due to consent prefs>[/color]

custom-examine-cant-interact = You can't interact with this entity.
custom-examine-cant-change-data-generic = You cannot change custom examine of this entity.
custom-examine-cant-change-data-consent = The user did not consent for this.
# This is the best I could come up with. I thought about "something has changed" but that just sounds ominous and would confuse new players.
custom-examine-data-changed-visibly = Custom examine has changed.
custom-examine-do-after-started-target = {THE($user)} is trying to change your custom examine data.
7 changes: 7 additions & 0 deletions Resources/Prototypes/_Floof/consent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
id: NSFWDescriptions
name: NSFW character descriptions
description: Toggling this on allows you to see explicit examine messages added by other players.
sortKey: CE_Nsfw

- type: consentToggle
id: CustomExamineChangedByOthers
name: Allow others to change your examine
description: Toggle this on to allow other players to change your custom examine data, both public and subtle. There will be a small delay.
sortKey: CE_Cbe

- type: consentToggle
id: MassMindswap
Expand Down
Loading