mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 05:27:38 +03:00
 Resolves #1529 # Changelog <!-- You can add an author after the `🆑` to change the name that appears in the changelog (ex: `🆑 Death`) Leaving it blank will default to your GitHub display name This includes all available types for the changelog --> 🆑 - fix: Fixed interactions not respecting identity. - fix: Potentially fixed spawning issues. - fix: Fixed borgs, animals and aghosts being able to enter cryosleep. --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> (cherry picked from commit 0f51c3756ec939fa7edd8e637f09fe84999cafea)
436 lines
18 KiB
C#
436 lines
18 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Contests;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.Ghost;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.InteractionVerbs.Events;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Verbs;
|
|
using Content.Shared.Whitelist;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using static Content.Shared.InteractionVerbs.InteractionPopupPrototype.Prefix;
|
|
using static Content.Shared.InteractionVerbs.InteractionVerbPrototype.ContestType;
|
|
using static Content.Shared.InteractionVerbs.InteractionVerbPrototype.EffectTargetSpecifier;
|
|
|
|
namespace Content.Shared.InteractionVerbs;
|
|
|
|
public abstract class SharedInteractionVerbsSystem : EntitySystem
|
|
{
|
|
private readonly InteractionAction.VerbDependencies _verbDependencies = new();
|
|
private List<InteractionVerbPrototype> _globalPrototypes = default!;
|
|
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly SharedDoAfterSystem _doAfters = default!;
|
|
[Dependency] private readonly ContestsSystem _contests = default!;
|
|
[Dependency] private readonly INetManager _net = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popups = default!;
|
|
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
IoCManager.InjectDependencies(_verbDependencies);
|
|
|
|
LoadGlobalVerbs();
|
|
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
|
|
|
SubscribeLocalEvent<InteractionVerbsComponent, GetVerbsEvent<InteractionVerb>>(OnGetOthersVerbs);
|
|
SubscribeLocalEvent<OwnInteractionVerbsComponent, GetVerbsEvent<InnateVerb>>(OnGetOwnVerbs);
|
|
SubscribeLocalEvent<InteractionVerbDoAfterEvent>(OnDoAfterFinished);
|
|
}
|
|
|
|
private void LoadGlobalVerbs()
|
|
{
|
|
_globalPrototypes = _protoMan.EnumeratePrototypes<InteractionVerbPrototype>()
|
|
.Where(v => v is { Global: true, Abstract: false })
|
|
.ToList();
|
|
}
|
|
|
|
#region event handling
|
|
|
|
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
|
{
|
|
if (!args.WasModified<InteractionVerbPrototype>())
|
|
return;
|
|
|
|
LoadGlobalVerbs();
|
|
}
|
|
|
|
private void OnGetOthersVerbs(Entity<InteractionVerbsComponent> entity, ref GetVerbsEvent<InteractionVerb> args)
|
|
{
|
|
// Global verbs are not added here since OnGetOwnVerbs already adds them
|
|
AddAll(entity.Comp.AllowedVerbs.Select(_protoMan.Index), args, () => new InteractionVerb());
|
|
}
|
|
|
|
private void OnGetOwnVerbs(Entity<OwnInteractionVerbsComponent> entity, ref GetVerbsEvent<InnateVerb> args)
|
|
{
|
|
var allVerbs = entity.Comp.AllowedVerbs;
|
|
|
|
var getVerbsEv = new GetInteractionVerbsEvent(allVerbs);
|
|
RaiseLocalEvent(entity, ref getVerbsEv);
|
|
|
|
// Global verbs are added here because they should be allowed even on entities that do not define any interactions
|
|
AddAll(allVerbs.Select(_protoMan.Index).Union(_globalPrototypes), args, () => new InnateVerb());
|
|
}
|
|
|
|
private void OnDoAfterFinished(InteractionVerbDoAfterEvent ev)
|
|
{
|
|
if (ev.Cancelled || ev.Handled || !_protoMan.TryIndex(ev.VerbPrototype, out var proto))
|
|
return;
|
|
|
|
PerformVerb(proto, ev.VerbArgs!);
|
|
ev.Handled = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public api
|
|
|
|
/// <summary>
|
|
/// Starts the verb, checking if it can be performed first, unless forced.
|
|
/// Upon success, this method will either start a do-after, or pass control to <see cref="PerformVerb"/>.
|
|
/// </summary>
|
|
// TODO this function is an active battlefield
|
|
public bool StartVerb(InteractionVerbPrototype proto, InteractionArgs args, bool force = false)
|
|
{
|
|
if (!TryComp<OwnInteractionVerbsComponent>(args.User, out var ownInteractions)
|
|
|| !force && !CheckVerbCooldown(proto, args, out _, ownInteractions))
|
|
return false;
|
|
|
|
// If contest advantage wasn't calculated yet, calculate it now and ensure it's in the allowed range
|
|
var contestAdvantageValid = true;
|
|
if (args.ContestAdvantage is null)
|
|
CalculateAdvantage(proto, ref args, out contestAdvantageValid);
|
|
|
|
if (!_net.IsClient
|
|
&& !force
|
|
&& (!contestAdvantageValid || proto.Action?.CanPerform(args, proto, true, _verbDependencies) != true))
|
|
{
|
|
CreateVerbEffects(proto.EffectFailure, Fail, proto, args);
|
|
return false;
|
|
}
|
|
|
|
var attemptEv = new InteractionVerbAttemptEvent(proto, args);
|
|
RaiseLocalEvent(args.User, ref attemptEv);
|
|
RaiseLocalEvent(args.Target, ref attemptEv);
|
|
|
|
if (attemptEv.Cancelled)
|
|
{
|
|
CreateVerbEffects(proto.EffectFailure, Fail, proto, args);
|
|
return false;
|
|
}
|
|
if (attemptEv.Handled)
|
|
return true;
|
|
|
|
var cooldown = proto.Cooldown;
|
|
var delay = proto.Delay;
|
|
if (proto.ContestDelay)
|
|
delay /= args.ContestAdvantage!.Value;
|
|
if (proto.ContestCooldown)
|
|
cooldown /= args.ContestAdvantage!.Value;
|
|
|
|
StartVerbCooldown(proto, args, cooldown, ownInteractions);
|
|
|
|
// Delay can become zero if the contest advantage is infinity or just really large...
|
|
if (delay <= TimeSpan.Zero)
|
|
{
|
|
PerformVerb(proto, args);
|
|
return true;
|
|
}
|
|
|
|
var doAfter = new DoAfterArgs(proto.DoAfter)
|
|
{
|
|
User = args.User,
|
|
Target = args.Target,
|
|
EventTarget = EntityUid.Invalid, // Raised broadcast
|
|
Broadcast = true,
|
|
BreakOnHandChange = proto.RequiresHands,
|
|
NeedHand = proto.RequiresHands,
|
|
RequireCanInteract = proto.RequiresCanAccess,
|
|
Delay = delay,
|
|
Event = new InteractionVerbDoAfterEvent(proto.ID, args)
|
|
};
|
|
|
|
var isSuccess = _doAfters.TryStartDoAfter(doAfter);
|
|
if (isSuccess)
|
|
CreateVerbEffects(proto.EffectDelayed, Delayed, proto, args);
|
|
|
|
return isSuccess;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an additional CanPerform check (unless forced) and then actually performs the action of the verb
|
|
/// and shows a success/failure popup.
|
|
/// </summary>
|
|
/// <remarks>This does nothing on client, as the client has no clue about verb actions. Only the server should ever perform verbs.</remarks>
|
|
public void PerformVerb(InteractionVerbPrototype proto, InteractionArgs args, bool force = false)
|
|
{
|
|
if (_net.IsClient)
|
|
return; // this leads to issues
|
|
|
|
if (!PerformChecks(proto, ref args, out _, out _) && !force
|
|
|| !proto.Action!.CanPerform(args, proto, false, _verbDependencies) && !force
|
|
|| !proto.Action.Perform(args, proto, _verbDependencies))
|
|
{
|
|
CreateVerbEffects(proto.EffectFailure, Fail, proto, args);
|
|
return;
|
|
}
|
|
|
|
CreateVerbEffects(proto.EffectSuccess, Success, proto, args);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region private api
|
|
|
|
/// <summary>
|
|
/// Creates verbs for all listed prototypes that match their own requirements. Uses the provided factory to create new verb instances.
|
|
/// </summary>
|
|
// Note: using `where T : Verb, new()` here results in a sandbox violation... Yea we peasants don't get OOP in ss14.
|
|
private void AddAll<T>(IEnumerable<InteractionVerbPrototype> verbs, GetVerbsEvent<T> args, Func<T> factory) where T : Verb
|
|
{
|
|
// Don't add verbs to ghosts. Ghost system will also cancel all verbs by/on non-admin ghosts.
|
|
if (TryComp<GhostComponent>(args.User, out var ghost) && !ghost.CanGhostInteract)
|
|
return;
|
|
|
|
var ownInteractions = EnsureComp<OwnInteractionVerbsComponent>(args.User);
|
|
foreach (var proto in verbs)
|
|
{
|
|
DebugTools.AssertNotEqual(proto.Abstract, true, "Attempted to add a verb with an abstract prototype.");
|
|
|
|
var name = proto.Name;
|
|
if (args.Verbs.Any(v => v.Text == name))
|
|
continue;
|
|
|
|
var verbArgs = InteractionArgs.From(args);
|
|
var isEnabled = PerformChecks(proto, ref verbArgs, out var skipAdding, out var errorLocale);
|
|
|
|
if (skipAdding)
|
|
continue;
|
|
|
|
var verb = factory.Invoke();
|
|
CopyVerbData(proto, verb);
|
|
verb.Act = () => StartVerb(proto, verbArgs);
|
|
verb.Disabled = !isEnabled;
|
|
|
|
if (!isEnabled)
|
|
verb.Message = Loc.GetString(errorLocale!);
|
|
|
|
if (isEnabled && !CheckVerbCooldown(proto, verbArgs, out var remainingTime, ownInteractions))
|
|
{
|
|
verb.Disabled = true;
|
|
verb.Message = Loc.GetString("interaction-verb-cooldown", ("seconds", remainingTime.TotalSeconds));
|
|
}
|
|
|
|
args.Verbs.Add(verb);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs all requirement/action checks on the verb. Returns true if the verb can be executed right now.
|
|
/// The skipAdding output param indicates whether the caller should skip adding this verb to the verb list, if applicable.
|
|
/// </summary>
|
|
private bool PerformChecks(InteractionVerbPrototype proto, ref InteractionArgs args, out bool skipAdding, [NotNullWhen(false)] out string? errorLocale)
|
|
{
|
|
if (!proto.AllowSelfInteract && args.User == args.Target
|
|
|| !Transform(args.User).Coordinates.TryDistance(EntityManager, Transform(args.Target).Coordinates, out var distance))
|
|
{
|
|
skipAdding = true;
|
|
errorLocale = "interaction-verb-invalid-target";
|
|
return false;
|
|
}
|
|
|
|
if (proto.Requirement?.IsMet(args, proto, _verbDependencies) == false)
|
|
{
|
|
skipAdding = proto.HideByRequirement;
|
|
errorLocale = "interaction-verb-invalid";
|
|
return false;
|
|
}
|
|
|
|
// TODO: we skip this check since the client is not aware of actions. This should be changed, maybe make actions mixed server/client?
|
|
if (proto.Action?.IsAllowed(args, proto, _verbDependencies) != true && !_net.IsClient)
|
|
{
|
|
skipAdding = proto.HideWhenInvalid;
|
|
errorLocale = "interaction-verb-invalid";
|
|
return false;
|
|
}
|
|
|
|
skipAdding = false;
|
|
if (proto.RequiresHands && !args.HasHands)
|
|
{
|
|
errorLocale = "interaction-verb-no-hands";
|
|
return false;
|
|
}
|
|
|
|
if (!args.CanInteract || proto.RequiresCanAccess && !args.CanAccess || !proto.Range.IsInRange(distance))
|
|
{
|
|
errorLocale = "interaction-verb-cannot-reach";
|
|
return false;
|
|
}
|
|
|
|
// Calculate contest advantage early if required
|
|
if (proto.ContestAdvantageRange is not null)
|
|
{
|
|
CalculateAdvantage(proto, ref args, out var canPerform);
|
|
|
|
if (!canPerform)
|
|
{
|
|
errorLocale = "interaction-verb-too-" + (args.ContestAdvantage > 1f ? "strong" : "weak");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
errorLocale = null;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the effective contest advantage for the verb and writes their clamped value to <see cref="InteractionArgs.ContestAdvantage"/>.
|
|
/// </summary>
|
|
private void CalculateAdvantage(InteractionVerbPrototype proto, ref InteractionArgs args, out bool canPerform)
|
|
{
|
|
args.ContestAdvantage = 1f;
|
|
canPerform = true;
|
|
|
|
var contests = proto.AllowedContests;
|
|
if (contests == None)
|
|
return;
|
|
|
|
// We don't use EveryContest here because it's straight up bad
|
|
if (contests.HasFlag(Mass))
|
|
args.ContestAdvantage *= _contests.MassContest(args.User, args.Target, true, 10f);
|
|
if (contests.HasFlag(Stamina))
|
|
args.ContestAdvantage *= _contests.MassContest(args.User, args.Target, true, 10f);
|
|
if (contests.HasFlag(Health))
|
|
args.ContestAdvantage *= _contests.MassContest(args.User, args.Target, true, 10f);
|
|
|
|
canPerform = proto.ContestAdvantageRange?.IsInRange(args.ContestAdvantage.Value) ?? true;
|
|
args.ContestAdvantage = proto.ContestAdvantageLimit.Clamp(args.ContestAdvantage.Value);
|
|
}
|
|
|
|
private void CopyVerbData(InteractionVerbPrototype proto, Verb verb)
|
|
{
|
|
verb.Text = proto.Name;
|
|
verb.Message = proto.Description;
|
|
verb.DoContactInteraction = proto.DoContactInteraction;
|
|
verb.Priority = proto.Priority;
|
|
verb.Icon = proto.Icon;
|
|
verb.Category = VerbCategory.Interaction;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the verb is on cooldown. Returns true if the verb can be used right now.
|
|
/// </summary>
|
|
private bool CheckVerbCooldown(InteractionVerbPrototype proto, InteractionArgs args, out TimeSpan remainingTime, OwnInteractionVerbsComponent? comp = null)
|
|
{
|
|
remainingTime = TimeSpan.Zero;
|
|
if (!Resolve(args.User, ref comp))
|
|
return false;
|
|
|
|
var cooldownTarget = proto.GlobalCooldown ? EntityUid.Invalid : args.Target;
|
|
if (!comp.Cooldowns.TryGetValue((proto.ID, cooldownTarget), out var cooldown))
|
|
return true;
|
|
|
|
remainingTime = cooldown - _timing.CurTime;
|
|
return remainingTime <= TimeSpan.Zero;
|
|
}
|
|
|
|
private void StartVerbCooldown(InteractionVerbPrototype proto, InteractionArgs args, TimeSpan cooldown, OwnInteractionVerbsComponent? comp = null)
|
|
{
|
|
if (!Resolve(args.User, ref comp))
|
|
return;
|
|
|
|
var cooldownTarget = proto.GlobalCooldown ? EntityUid.Invalid : args.Target;
|
|
comp.Cooldowns[(proto.ID, cooldownTarget)] = _timing.CurTime + cooldown;
|
|
|
|
// We also clean up old cooldowns here to avoid a memory leak... This is probably a bad place to do it.
|
|
// TODO might wanna switch to a list because dict is probably overkill for this task given we clean it up often.
|
|
foreach (var (key, time) in comp.Cooldowns.ToArray())
|
|
{
|
|
if (time < _timing.CurTime)
|
|
comp.Cooldowns.Remove(key);
|
|
}
|
|
}
|
|
|
|
private void CreateVerbEffects(InteractionVerbPrototype.EffectSpecifier? specifier, InteractionPopupPrototype.Prefix prefix, InteractionVerbPrototype proto, InteractionArgs args)
|
|
{
|
|
// Not doing effects on client because it causes issues
|
|
if (specifier is null || _net.IsClient)
|
|
return;
|
|
|
|
var (user, target, used) = (args.User, args.Target, args.Used);
|
|
|
|
// Effect targets for different players
|
|
var userTarget = specifier.EffectTarget is User or UserThenTarget or TargetThenUser ? user : target;
|
|
var targetTarget = specifier.EffectTarget is Target or UserThenTarget or TargetThenUser ? target : user;
|
|
var othersTarget = specifier.EffectTarget is Target or UserThenTarget ? target : user;
|
|
var othersFilter = Filter.Pvs(othersTarget).RemoveWhereAttachedEntity(ent => ent == user || ent == target);
|
|
|
|
// Popups
|
|
if (_protoMan.TryIndex(specifier.Popup, out var popup))
|
|
{
|
|
var locPrefix = $"interaction-{proto.ID}-{prefix.ToString().ToLower()}";
|
|
|
|
(string, object)[] localeArgs =
|
|
[
|
|
("user", Identity.Entity(user, _entityManager)),
|
|
("target", Identity.Entity(target, _entityManager)),
|
|
("used", used ?? EntityUid.Invalid),
|
|
("selfTarget", user == target),
|
|
("hasUsed", used != null)
|
|
];
|
|
|
|
// User popup
|
|
var userSuffix = popup.SelfSuffix ?? popup.OthersSuffix;
|
|
if (userSuffix is not null)
|
|
PopupEffects(Loc.GetString($"{locPrefix}-{userSuffix}-popup", localeArgs), userTarget, Filter.Entities(user), false, popup);
|
|
|
|
// Target popup
|
|
var targetSuffix = popup.TargetSuffix ?? popup.OthersSuffix;
|
|
if (targetSuffix is not null && user != target)
|
|
PopupEffects(Loc.GetString($"{locPrefix}-{targetSuffix}-popup", localeArgs), targetTarget, Filter.Entities(target), false, popup);
|
|
|
|
// Others popup
|
|
var othersSuffix = popup.OthersSuffix;
|
|
if (othersSuffix is not null)
|
|
PopupEffects(Loc.GetString($"{locPrefix}-{othersSuffix}-popup", localeArgs), othersTarget, othersFilter, true, popup, clip: true);
|
|
}
|
|
|
|
// Sounds
|
|
if (specifier.Sound is { } sound)
|
|
{
|
|
// TODO we have a choice between having an accurate sound source or saving on an entity spawn...
|
|
_audio.PlayEntity(sound, Filter.Entities(user, target), target, false, specifier.SoundParams);
|
|
|
|
if (specifier.SoundPerceivedByOthers)
|
|
_audio.PlayEntity(sound, othersFilter, othersTarget, false, specifier.SoundParams);
|
|
}
|
|
}
|
|
|
|
private void PopupEffects(string message, EntityUid target, Filter filter, bool recordReplay, InteractionPopupPrototype popup, bool clip = false)
|
|
{
|
|
// Sending a chat message will result in a popup anyway
|
|
// TODO this needs to be fixed probably. Popups and chat messages should be independent.
|
|
if (popup.LogPopup)
|
|
SendChatLog(message, target, filter, popup, clip);
|
|
else
|
|
_popups.PopupEntity(message, target, filter, recordReplay, popup.PopupType);
|
|
}
|
|
|
|
protected virtual void SendChatLog(string message, EntityUid source, Filter filter, InteractionPopupPrototype popup, bool clip)
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
}
|