diff --git a/Content.Client/GameObjects/EntitySystems/VerbSystem.cs b/Content.Client/GameObjects/EntitySystems/VerbSystem.cs index 026a7e9fb1..9dfbebd8d6 100644 --- a/Content.Client/GameObjects/EntitySystems/VerbSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/VerbSystem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using Content.Shared.GameObjects; using Content.Shared.GameObjects.EntitySystemMessages; using Content.Shared.Input; @@ -160,41 +161,25 @@ namespace Content.Client.GameObjects.EntitySystems } var user = GetUserEntity(); + //Get verbs, component dependent. foreach (var (component, verb) in VerbUtility.GetVerbs(entity)) { - if (verb.RequireInteractionRange) - { - var distanceSquared = (user.Transform.WorldPosition - entity.Transform.WorldPosition) - .LengthSquared; - if (distanceSquared > Verb.InteractionRangeSquared) - { - continue; - } - } + if (verb.RequireInteractionRange && !VerbUtility.InVerbUseRange(user, entity)) + continue; var disabled = verb.GetVisibility(user, component) != VerbVisibility.Visible; - var button = new Button - { - Text = verb.GetText(user, component), - Disabled = disabled - }; - if (!disabled) - { - button.OnPressed += _ => - { - _closeContextMenu(); - try - { - verb.Activate(user, component); - } - catch (Exception e) - { - Logger.ErrorS("verb", "Exception in verb {0} on {1}:\n{2}", verb, entity, e); - } - }; - } + buttons.Add(CreateVerbButton(verb.GetText(user, component), disabled, verb.ToString(), + entity.ToString(), () => verb.Activate(user, component))); + } + //Get global verbs. Visible for all entities regardless of their components. + foreach (var globalVerb in VerbUtility.GetGlobalVerbs(Assembly.GetExecutingAssembly())) + { + if (globalVerb.RequireInteractionRange && !VerbUtility.InVerbUseRange(user, entity)) + continue; - buttons.Add(button); + var disabled = globalVerb.GetVisibility(user, entity) != VerbVisibility.Visible; + buttons.Add(CreateVerbButton(globalVerb.GetText(user, entity), disabled, globalVerb.ToString(), + entity.ToString(), () => globalVerb.Activate(user, entity))); } if (buttons.Count > 0) @@ -224,6 +209,31 @@ namespace Content.Client.GameObjects.EntitySystems } } + private Button CreateVerbButton(string text, bool disabled, string verbName, string ownerName, Action action) + { + var button = new Button + { + Text = text, + Disabled = disabled + }; + if (!disabled) + { + button.OnPressed += _ => + { + _closeContextMenu(); + try + { + action.Invoke(); + } + catch (Exception e) + { + Logger.ErrorS("verb", "Exception in verb {0} on {1}:\n{2}", verbName, ownerName, e); + } + }; + } + return button; + } + private void _closeContextMenu() { _currentPopup?.Dispose(); diff --git a/Content.Client/GlobalVerbs/ViewVariablesVerb.cs b/Content.Client/GlobalVerbs/ViewVariablesVerb.cs new file mode 100644 index 0000000000..482f615dc9 --- /dev/null +++ b/Content.Client/GlobalVerbs/ViewVariablesVerb.cs @@ -0,0 +1,32 @@ +using Content.Shared.GameObjects; +using Robust.Client.Console; +using Robust.Client.ViewVariables; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Client.GlobalVerbs +{ + /// + /// Global verb that opens a view variables window for the entity in question. + /// + [GlobalVerb] + class ViewVariablesVerb : GlobalVerb + { + public override string GetText(IEntity user, IEntity target) => "View variables"; + public override bool RequireInteractionRange => false; + + public override VerbVisibility GetVisibility(IEntity user, IEntity target) + { + var groupController = IoCManager.Resolve(); + if (groupController.CanViewVar()) + return VerbVisibility.Visible; + return VerbVisibility.Invisible; + } + + public override void Activate(IEntity user, IEntity target) + { + var vvm = IoCManager.Resolve(); + vvm.OpenVV(target); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/VerbSystem.cs b/Content.Server/GameObjects/EntitySystems/VerbSystem.cs index fbf68f1edf..8214cb9a9f 100644 --- a/Content.Server/GameObjects/EntitySystems/VerbSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/VerbSystem.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Reflection; using Content.Shared.GameObjects; using Content.Shared.GameObjects.EntitySystemMessages; using Robust.Server.Interfaces.Player; @@ -50,20 +51,12 @@ namespace Content.Server.GameObjects.EntitySystems var userEntity = session.AttachedEntity; var data = new List(); + //Get verbs, component dependent. foreach (var (component, verb) in VerbUtility.GetVerbs(entity)) { - if (verb.RequireInteractionRange) - { - var distanceSquared = (userEntity.Transform.WorldPosition - entity.Transform.WorldPosition) - .LengthSquared; - if (distanceSquared > Verb.InteractionRangeSquared) - { - continue; - } - } - - var vis = verb.GetVisibility(userEntity, component); - if(vis == VerbVisibility.Invisible) + if (verb.RequireInteractionRange && !VerbUtility.InVerbUseRange(userEntity, entity)) + continue; + if(VerbUtility.IsVerbInvisible(verb, userEntity, component, out var vis)) continue; // TODO: These keys being giant strings is inefficient as hell. @@ -71,6 +64,17 @@ namespace Content.Server.GameObjects.EntitySystems $"{component.GetType()}:{verb.GetType()}", vis == VerbVisibility.Visible)); } + //Get global verbs. Visible for all entities regardless of their components. + foreach (var globalVerb in VerbUtility.GetGlobalVerbs(Assembly.GetExecutingAssembly())) + { + if (globalVerb.RequireInteractionRange && !VerbUtility.InVerbUseRange(userEntity, entity)) + continue; + if(VerbUtility.IsVerbInvisible(globalVerb, userEntity, entity, out var vis)) + continue; + + data.Add(new VerbsResponseMessage.VerbData(globalVerb.GetText(userEntity, entity), + globalVerb.GetType().ToString(), vis == VerbVisibility.Visible)); + } var response = new VerbsResponseMessage(data, req.EntityUid); RaiseNetworkEvent(response, channel); @@ -99,7 +103,7 @@ namespace Content.Server.GameObjects.EntitySystems { var distanceSquared = (userEntity.Transform.WorldPosition - entity.Transform.WorldPosition) .LengthSquared; - if (distanceSquared > Verb.InteractionRangeSquared) + if (distanceSquared > VerbUtility.InteractionRangeSquared) { break; } diff --git a/Content.Shared/GameObjects/Verbs/GlobalVerb.cs b/Content.Shared/GameObjects/Verbs/GlobalVerb.cs new file mode 100644 index 0000000000..a1f683828e --- /dev/null +++ b/Content.Shared/GameObjects/Verbs/GlobalVerb.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Shared.GameObjects +{ + /// + /// A verb is an action in the right click menu of an entity. + /// Global verbs are visible on all entities, regardless of their components. + /// + /// + /// To add a global verb to all entities, + /// define it and mark it with + /// + public abstract class GlobalVerb + { + /// + /// If true, this verb requires the user to be within + /// meters from the entity on which this verb resides. + /// + public virtual bool RequireInteractionRange => true; + + /// + /// Gets the text string that will be shown to in the right click menu. + /// + /// The entity of the user opening this menu. + /// The text string that is shown in the right click menu for this verb. + public abstract string GetText(IEntity user, IEntity target); + + /// + /// Gets the visibility level of this verb in the right click menu. + /// + /// The entity of the user opening this menu. + /// The visibility level of the verb in the client's right click menu. + public abstract VerbVisibility GetVisibility(IEntity user, IEntity target); + + /// + /// Invoked when this verb is activated from the right click menu. + /// + /// The entity of the user opening this menu. + /// The entity that is being acted upon. + public abstract void Activate(IEntity user, IEntity target); + } + + /// + /// This attribute should be used on . These are verbs which are on visible for all entities, + /// regardless of the components they contain. + /// + [MeansImplicitUse] + [BaseTypeRequired(typeof(GlobalVerb))] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class GlobalVerbAttribute : Attribute + { + } +} diff --git a/Content.Shared/GameObjects/Verb.cs b/Content.Shared/GameObjects/Verbs/Verb.cs similarity index 85% rename from Content.Shared/GameObjects/Verb.cs rename to Content.Shared/GameObjects/Verbs/Verb.cs index 18c47e9c46..d4f88ef12b 100644 --- a/Content.Shared/GameObjects/Verb.cs +++ b/Content.Shared/GameObjects/Verbs/Verb.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using JetBrains.Annotations; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Utility; @@ -22,9 +23,6 @@ namespace Content.Shared.GameObjects /// public virtual bool RequireInteractionRange => true; - public const float InteractionRange = 2; - public const float InteractionRangeSquared = InteractionRange * InteractionRange; - /// /// Gets the text string that will be shown to in the right click menu. /// @@ -109,29 +107,6 @@ namespace Content.Shared.GameObjects { } - public static class VerbUtility - { - // TODO: This is a quick hack. Verb objects should absolutely be cached properly. - // This works for now though. - public static IEnumerable<(IComponent, Verb)> GetVerbs(IEntity entity) - { - foreach (var component in entity.GetAllComponents()) - { - var type = component.GetType(); - foreach (var nestedType in type.GetAllNestedTypes()) - { - if (!typeof(Verb).IsAssignableFrom(nestedType) || nestedType.IsAbstract) - { - continue; - } - - var verb = (Verb) Activator.CreateInstance(nestedType); - yield return (component, verb); - } - } - } - } - /// /// Possible states of visibility for the verb in the right click menu. /// diff --git a/Content.Shared/GameObjects/Verbs/VerbUtility.cs b/Content.Shared/GameObjects/Verbs/VerbUtility.cs new file mode 100644 index 0000000000..aa76c66f77 --- /dev/null +++ b/Content.Shared/GameObjects/Verbs/VerbUtility.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Utility; + +namespace Content.Shared.GameObjects +{ + public static class VerbUtility + { + public const float InteractionRange = 2; + public const float InteractionRangeSquared = InteractionRange * InteractionRange; + + // TODO: This is a quick hack. Verb objects should absolutely be cached properly. + // This works for now though. + public static IEnumerable<(IComponent, Verb)> GetVerbs(IEntity entity) + { + foreach (var component in entity.GetAllComponents()) + { + var type = component.GetType(); + foreach (var nestedType in type.GetAllNestedTypes()) + { + if (!typeof(Verb).IsAssignableFrom(nestedType) || nestedType.IsAbstract) + { + continue; + } + + var verb = (Verb)Activator.CreateInstance(nestedType); + yield return (component, verb); + } + } + } + + /// + /// Returns an IEnumerable of all classes inheriting with the attribute. + /// + /// The assembly to search for global verbs in. + public static IEnumerable GetGlobalVerbs(Assembly assembly) + { + foreach (Type type in assembly.GetTypes()) + { + if (Attribute.IsDefined(type, typeof(GlobalVerbAttribute))) + { + if (!typeof(GlobalVerb).IsAssignableFrom(type) || type.IsAbstract) + { + continue; + } + yield return (GlobalVerb)Activator.CreateInstance(type); + } + } + } + + public static bool InVerbUseRange(IEntity user, IEntity target) + { + var distanceSquared = (user.Transform.WorldPosition - target.Transform.WorldPosition) + .LengthSquared; + if (distanceSquared > InteractionRangeSquared) + { + return false; + } + return true; + } + + public static bool IsVerbInvisible(Verb verb, IEntity user, IComponent target, out VerbVisibility visibility) + { + visibility = verb.GetVisibility(user, target); + return visibility == VerbVisibility.Invisible; + } + + public static bool IsVerbInvisible(GlobalVerb verb, IEntity user, IEntity target, out VerbVisibility visibility) + { + visibility = verb.GetVisibility(user, target); + return visibility == VerbVisibility.Invisible; + } + } +}