diff --git a/DMCompiler/Compiler/DM/DMAST.cs b/DMCompiler/Compiler/DM/DMAST.cs index faf4914cf9..e80b68851e 100644 --- a/DMCompiler/Compiler/DM/DMAST.cs +++ b/DMCompiler/Compiler/DM/DMAST.cs @@ -1769,10 +1769,10 @@ public override void Visit(DMASTVisitor visitor) { } public sealed class DMASTNewPath : DMASTExpression { - public readonly DMASTPath Path; + public readonly DMASTConstantPath Path; public readonly DMASTCallParameter[] Parameters; - public DMASTNewPath(Location location, DMASTPath path, DMASTCallParameter[] parameters) : base(location) { + public DMASTNewPath(Location location, DMASTConstantPath path, DMASTCallParameter[] parameters) : base(location) { Path = path; Parameters = parameters; } diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index fca532a14a..d0de271eae 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -2082,7 +2082,7 @@ public void ExpressionTo(out DMASTExpression? endRange, out DMASTExpression? ste DMASTCallParameter[]? parameters = ProcCall(); DMASTExpression? newExpression = type switch { - DMASTConstantPath path => new DMASTNewPath(loc, path.Value, parameters), + DMASTConstantPath path => new DMASTNewPath(loc, path, parameters), DMASTExpression expr => new DMASTNewExpr(loc, expr, parameters), null => new DMASTNewInferred(loc, parameters), }; diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 81b9922660..f24dcdd1f9 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -69,6 +69,7 @@ public DMProcScope(DMProcScope? parentScope) { public int Id; public Dictionary GlobalVariables = new(); + public VerbSrc? VerbSrc; public string? VerbName; public string? VerbCategory = string.Empty; public string? VerbDesc; @@ -140,6 +141,7 @@ public ProcDefinitionJson GetJsonRepresentation() { procDefinition.Attributes = Attributes; } + procDefinition.VerbSrc = VerbSrc; procDefinition.VerbName = VerbName; // Normally VerbCategory is "" by default and null to hide it, but we invert those during (de)serialization to reduce JSON size VerbCategory = VerbCategory switch { diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index 3a3a0abc5b..3880a5bce0 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -63,29 +63,33 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { } // new /x/y/z (...) - sealed class NewPath : DMExpression { - private readonly DreamPath _targetPath; - private readonly ArgumentList _arguments; - - public NewPath(Location location, DreamPath targetPath, ArgumentList arguments) : base(location) { - _targetPath = targetPath; - _arguments = arguments; - } + internal sealed class NewPath(Location location, Path targetPath, ArgumentList arguments) : DMExpression(location) { + public override DreamPath? Path => targetPath.Value; public override void EmitPushValue(DMObject dmObject, DMProc proc) { - if (!DMObjectTree.TryGetTypeId(_targetPath, out var typeId)) { - DMCompiler.Emit(WarningCode.ItemDoesntExist, Location, $"Type {_targetPath} does not exist"); - + if (!targetPath.TryResolvePath(out var pathInfo)) { + proc.PushNull(); return; } - var argumentInfo = _arguments.EmitArguments(dmObject, proc); + var argumentInfo = arguments.EmitArguments(dmObject, proc); + + switch (pathInfo.Value.Type) { + case Expressions.Path.PathType.TypeReference: + proc.PushType(pathInfo.Value.Id); + break; + case Expressions.Path.PathType.ProcReference: // "new /proc/new_verb(Destination)" is a thing + proc.PushProc(pathInfo.Value.Id); + break; + case Expressions.Path.PathType.ProcStub: + case Expressions.Path.PathType.VerbStub: + DMCompiler.Emit(WarningCode.BadExpression, Location, "Cannot use \"new\" with a proc stub"); + proc.PushNull(); + return; + } - proc.PushType(typeId); proc.CreateObject(argumentInfo.Type, argumentInfo.StackSize); } - - public override DreamPath? Path => _targetPath; } // locate() diff --git a/DMCompiler/DM/Expressions/Constant.cs b/DMCompiler/DM/Expressions/Constant.cs index 44c49246c9..79094c2a48 100644 --- a/DMCompiler/DM/Expressions/Constant.cs +++ b/DMCompiler/DM/Expressions/Constant.cs @@ -477,26 +477,21 @@ public override bool TryAsJsonRepresentation(out object? json) { } // /a/b/c - sealed class Path : Constant { - public DreamPath Value { get; } + internal sealed class Path(Location location, DMObject dmObject, DreamPath value) : Constant(location) { + public DreamPath Value { get; } = value; /// /// The DMObject this expression resides in. Used for path searches. /// - private readonly DMObject _dmObject; + private readonly DMObject _dmObject = dmObject; - private enum PathType { + public enum PathType { TypeReference, ProcReference, ProcStub, VerbStub } - public Path(Location location, DMObject dmObject, DreamPath value) : base(location) { - Value = value; - _dmObject = dmObject; - } - public override void EmitPushValue(DMObject dmObject, DMProc proc) { if (!TryResolvePath(out var pathInfo)) { proc.PushNull(); @@ -554,7 +549,7 @@ public override bool TryAsJsonRepresentation(out object? json) { return true; } - private bool TryResolvePath([NotNullWhen(true)] out (PathType Type, int Id)? pathInfo) { + public bool TryResolvePath([NotNullWhen(true)] out (PathType Type, int Id)? pathInfo) { DreamPath path = Value; // An upward search with no left-hand side diff --git a/DMCompiler/DM/VerbSrc.cs b/DMCompiler/DM/VerbSrc.cs new file mode 100644 index 0000000000..1730d9e134 --- /dev/null +++ b/DMCompiler/DM/VerbSrc.cs @@ -0,0 +1,21 @@ +namespace DMCompiler.DM; + +/// +/// The value of "set src = ..." in a verb +/// +public enum VerbSrc { + View, + InView, + OView, + InOView, + Range, + InRange, + ORange, + InORange, + World, + InWorld, + Usr, + InUsr, + UsrLoc, + UsrGroup +} diff --git a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs index 9823271b65..3ba04073a4 100644 --- a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs @@ -193,18 +193,24 @@ public static DMExpression BuildExpression(DMASTExpression expression, DMObject BuildExpression(ternary.B, dmObject, proc, inferredPath), BuildExpression(ternary.C ?? new DMASTConstantNull(ternary.Location), dmObject, proc, inferredPath)); case DMASTNewPath newPath: - return new NewPath(newPath.Location, - newPath.Path.Path, + if (BuildExpression(newPath.Path, dmObject, proc, inferredPath) is not Path path) { + DMCompiler.Emit(WarningCode.BadExpression, newPath.Path.Location, "Expected a path expression"); + return new Null(newPath.Location); + } + + return new NewPath(newPath.Location, path, new ArgumentList(newPath.Location, dmObject, proc, newPath.Parameters, inferredPath)); case DMASTNewExpr newExpr: return new New(newExpr.Location, BuildExpression(newExpr.Expression, dmObject, proc, inferredPath), new ArgumentList(newExpr.Location, dmObject, proc, newExpr.Parameters, inferredPath)); case DMASTNewInferred newInferred: - if (inferredPath is null) - throw new CompileErrorException(newInferred.Location, "An inferred new requires a type!"); - return new NewPath(newInferred.Location, - inferredPath.Value, + if (inferredPath is null) { + DMCompiler.Emit(WarningCode.BadExpression, newInferred.Location, "Could not infer a type"); + return new Null(newInferred.Location); + } + + return new NewPath(newInferred.Location, new Path(newInferred.Location, dmObject, inferredPath.Value), new ArgumentList(newInferred.Location, dmObject, proc, newInferred.Parameters, inferredPath)); case DMASTPreIncrement preIncrement: return new PreIncrement(preIncrement.Location, BuildExpression(preIncrement.Expression, dmObject, proc, inferredPath)); diff --git a/DMCompiler/DM/Visitors/DMProcBuilder.cs b/DMCompiler/DM/Visitors/DMProcBuilder.cs index 83d232faa6..60bd7a26da 100644 --- a/DMCompiler/DM/Visitors/DMProcBuilder.cs +++ b/DMCompiler/DM/Visitors/DMProcBuilder.cs @@ -186,9 +186,73 @@ public void ProcessStatementBreak(DMASTProcStatementBreak statementBreak) { public void ProcessStatementSet(DMASTProcStatementSet statementSet) { var attribute = statementSet.Attribute.ToLower(); - // TODO deal with "src" if(attribute == "src") { - DMCompiler.UnimplementedWarning(statementSet.Location, "'set src' is unimplemented"); + // TODO: Would be much better if the parser was just more strict with the expression + switch (statementSet.Value) { + case DMASTIdentifier {Identifier: "usr"}: + _proc.VerbSrc = statementSet.WasInKeyword ? VerbSrc.InUsr : VerbSrc.Usr; + if (statementSet.WasInKeyword) + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = usr.contents' is unimplemented"); + break; + case DMASTDereference {Expression: DMASTIdentifier{Identifier: "usr"}, Operations: var operations}: + if (operations is not [DMASTDereference.FieldOperation {Identifier: var deref}]) + goto default; + + if (deref == "contents") { + _proc.VerbSrc = VerbSrc.InUsr; + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = usr.contents' is unimplemented"); + } else if (deref == "loc") { + _proc.VerbSrc = VerbSrc.UsrLoc; + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = usr.loc' is unimplemented"); + } else if (deref == "group") { + _proc.VerbSrc = VerbSrc.UsrGroup; + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = usr.group' is unimplemented"); + } else { + goto default; + } + + break; + case DMASTIdentifier {Identifier: "world"}: + _proc.VerbSrc = statementSet.WasInKeyword ? VerbSrc.InWorld : VerbSrc.World; + if (statementSet.WasInKeyword) + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = world.contents' is unimplemented"); + else + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = world' is unimplemented"); + break; + case DMASTDereference {Expression: DMASTIdentifier{Identifier: "world"}, Operations: var operations}: + if (operations is not [DMASTDereference.FieldOperation {Identifier: "contents"}]) + goto default; + + _proc.VerbSrc = VerbSrc.InWorld; + DMCompiler.UnimplementedWarning(statementSet.Location, + "'set src = world.contents' is unimplemented"); + break; + case DMASTProcCall {Callable: DMASTCallableProcIdentifier {Identifier: { } viewType and ("view" or "oview")}}: + // TODO: Ranges + if (statementSet.WasInKeyword) + _proc.VerbSrc = viewType == "view" ? VerbSrc.InView : VerbSrc.InOView; + else + _proc.VerbSrc = viewType == "view" ? VerbSrc.View : VerbSrc.OView; + break; + // range() and orange() are undocumented, but they work + case DMASTProcCall {Callable: DMASTCallableProcIdentifier {Identifier: { } viewType and ("range" or "orange")}}: + // TODO: Ranges + if (statementSet.WasInKeyword) + _proc.VerbSrc = viewType == "range" ? VerbSrc.InRange : VerbSrc.InORange; + else + _proc.VerbSrc = viewType == "range" ? VerbSrc.Range : VerbSrc.ORange; + break; + default: + DMCompiler.Emit(WarningCode.BadExpression, statementSet.Value.Location, "Invalid verb src"); + break; + } + return; } diff --git a/DMCompiler/Json/DreamProcJson.cs b/DMCompiler/Json/DreamProcJson.cs index 5c26be6da2..8c1b38953c 100644 --- a/DMCompiler/Json/DreamProcJson.cs +++ b/DMCompiler/Json/DreamProcJson.cs @@ -14,8 +14,9 @@ public sealed class ProcDefinitionJson { public List SourceInfo { get; set; } public byte[]? Bytecode { get; set; } + public VerbSrc? VerbSrc { get; set; } public string? VerbName { get; set; } - public string? VerbCategory { get; set; } = null; + public string? VerbCategory { get; set; } public string? VerbDesc { get; set; } public sbyte Invisibility { get; set; } } diff --git a/OpenDream.sln.DotSettings b/OpenDream.sln.DotSettings index 854dfd7752..025cfa31f2 100644 --- a/OpenDream.sln.DotSettings +++ b/OpenDream.sln.DotSettings @@ -60,6 +60,7 @@ True True True + True True True True diff --git a/OpenDreamClient/ClientVerbSystem.cs b/OpenDreamClient/ClientVerbSystem.cs index 9d53745ef7..12851580fe 100644 --- a/OpenDreamClient/ClientVerbSystem.cs +++ b/OpenDreamClient/ClientVerbSystem.cs @@ -3,6 +3,7 @@ using OpenDreamClient.Rendering; using OpenDreamShared.Dream; using OpenDreamShared.Rendering; +using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Asynchronous; using Robust.Shared.Timing; @@ -15,6 +16,7 @@ public sealed class ClientVerbSystem : VerbSystem { [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly ITaskManager _taskManager = default!; [Dependency] private readonly ITimerManager _timerManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; private EntityQuery _spriteQuery; private EntityQuery _sightQuery; @@ -68,12 +70,12 @@ public IEnumerable GetAllVerbs() { /// Find all the verbs the client is currently capable of executing /// /// Whether to ignore "set hidden = TRUE" - /// The ID, target, and information of every executable verb + /// The ID, src, and information of every executable verb public IEnumerable<(int Id, ClientObjectReference Src, VerbInfo VerbInfo)> GetExecutableVerbs(bool ignoreHiddenAttr = false) { - DMISpriteComponent? playerSprite = null; + ClientObjectReference? ourMob = null; sbyte? seeInvisibility = null; if (_playerManager.LocalEntity != null) { - playerSprite = _spriteQuery.GetComponent(_playerManager.LocalEntity.Value); + ourMob = new ClientObjectReference(_entityManager.GetNetEntity(_playerManager.LocalEntity.Value)); seeInvisibility = _sightQuery.GetComponent(_playerManager.LocalEntity.Value).SeeInvisibility; } @@ -89,25 +91,78 @@ public IEnumerable GetAllVerbs() { } } - // Then, the verbs attached to our mob - if (playerSprite?.Icon.Appearance is { } playerAppearance) { - var playerNetEntity = _entityManager.GetNetEntity(_playerManager.LocalEntity); + // Then, the verbs on objects around us + var viewOverlay = _overlayManager.GetOverlay(); + foreach (var entity in viewOverlay.EntitiesInView) { + if (!_spriteQuery.TryGetComponent(entity, out var sprite)) + continue; + if (sprite.Icon.Appearance is not { } appearance) + continue; - if (playerNetEntity != null) { - foreach (var verbId in playerAppearance.Verbs) { - if (!_verbs.TryGetValue(verbId, out var verb)) - continue; - if (verb.IsHidden(ignoreHiddenAttr, seeInvisibility!.Value)) - continue; + foreach (var verbId in appearance.Verbs) { + if (!_verbs.TryGetValue(verbId, out var verb)) + continue; + if (verb.IsHidden(ignoreHiddenAttr, seeInvisibility!.Value)) + continue; + + var src = new ClientObjectReference(_entityManager.GetNetEntity(entity)); - yield return (verbId, new(playerNetEntity.Value), verb); + // Check the verb's "set src" allows us to execute this + switch (verb.Accessibility) { + case VerbAccessibility.Usr: + if (!src.Equals(ourMob)) + continue; + + break; + default: + // TODO: All the other kinds + break; } + + yield return (verbId, src, verb); } } + + // TODO: Turfs, Areas } - public bool TryGetVerbInfo(int verbId, out VerbInfo verbInfo) { - return _verbs.TryGetValue(verbId, out verbInfo); + /// + /// Find all the verbs the client is currently capable of executing on the given target + /// + /// The target of the verb + /// The ID, src, and information of every executable verb + public IEnumerable<(int Id, ClientObjectReference Src, VerbInfo VerbInfo)> GetExecutableVerbs(ClientObjectReference target) { + foreach (var verb in GetExecutableVerbs()) { + DreamValueType? targetType = verb.VerbInfo.GetTargetType(); + if (targetType == null) { + // Verbs without a target but an "in view()/range()" accessibility will still show + if (verb.Src.Equals(target) && + verb.VerbInfo.Accessibility + is VerbAccessibility.InRange or VerbAccessibility.InORange + or VerbAccessibility.InView or VerbAccessibility.InOView) + yield return verb; + + continue; + } + + switch (target.Type) { + case ClientObjectReference.RefType.Entity: + var entity = _entityManager.GetEntity(target.Entity); + var isMob = _entityManager.HasComponent(entity); + + if ((targetType & DreamValueType.Mob) != 0x0 && isMob) + yield return verb; + if ((targetType & DreamValueType.Obj) != 0x0 && !isMob) + yield return verb; + + break; + case ClientObjectReference.RefType.Turf: + if ((targetType & DreamValueType.Turf) != 0x0) + yield return verb; + + break; + } + } } /// diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs index 650afa6ba2..552e7ea8f1 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs @@ -1,4 +1,5 @@ using OpenDreamClient.Rendering; +using OpenDreamShared.Dream; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; @@ -10,17 +11,17 @@ namespace OpenDreamClient.Input.ContextMenu; internal sealed partial class ContextMenuItem : PanelContainer { private static readonly StyleBox HoverStyle = new StyleBoxFlat(Color.Gray); - public readonly EntityUid Entity; + public readonly ClientObjectReference Target; public readonly MetaDataComponent EntityMetaData; public readonly DMISpriteComponent? EntitySprite; private readonly ContextMenuPopup _menu; - public ContextMenuItem(ContextMenuPopup menu, EntityUid entity, MetaDataComponent metadata, DMISpriteComponent sprite) { + public ContextMenuItem(ContextMenuPopup menu, ClientObjectReference target, MetaDataComponent metadata, DMISpriteComponent sprite) { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - Entity = entity; + Target = target; EntityMetaData = metadata; EntitySprite = sprite; _menu = menu; diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index d12cad55c7..463b512c5e 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -49,7 +49,7 @@ public void RepopulateEntities(IEnumerable entities) { var metadata = _entityManager.GetComponent(entity); - ContextMenu.AddChild(new ContextMenuItem(this, entity, metadata, sprite)); + ContextMenu.AddChild(new ContextMenuItem(this, new(_entityManager.GetNetEntity(entity)), metadata, sprite)); } } @@ -59,7 +59,7 @@ public void SetActiveItem(ContextMenuItem item) { _uiManager.ModalRoot.RemoveChild(_currentVerbMenu); } - _currentVerbMenu = new VerbMenuPopup(_entityManager, _verbSystem, GetSeeInvisible(), item.Entity, item.EntityMetaData, item.EntitySprite); + _currentVerbMenu = new VerbMenuPopup(_verbSystem, GetSeeInvisible(), item.Target, item.EntityMetaData, item.EntitySprite); _currentVerbMenu.OnVerbSelected += Close; diff --git a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs index e75db414f4..921da9db2a 100644 --- a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs @@ -12,17 +12,15 @@ internal sealed partial class VerbMenuPopup : Popup { public VerbSelectedHandler? OnVerbSelected; - private readonly IEntityManager _entityManager; private readonly ClientVerbSystem? _verbSystem; - private readonly EntityUid _entity; + private readonly ClientObjectReference _target; - public VerbMenuPopup(IEntityManager entityManager, ClientVerbSystem? verbSystem, sbyte seeInvisible, EntityUid entity, MetaDataComponent? entityMetaData, DMISpriteComponent? entitySprite) { + public VerbMenuPopup(ClientVerbSystem? verbSystem, sbyte seeInvisible, ClientObjectReference target, MetaDataComponent? entityMetaData, DMISpriteComponent? entitySprite) { RobustXamlLoader.Load(this); - _entityManager = entityManager; _verbSystem = verbSystem; - _entity = entity; + _target = target; if (entityMetaData != null && !string.IsNullOrEmpty(entityMetaData.EntityDescription)) { DescLabel.Margin = new Thickness(4, 0, 4, 0); @@ -31,26 +29,25 @@ public VerbMenuPopup(IEntityManager entityManager, ClientVerbSystem? verbSystem, Desc.Visible = false; } - if (verbSystem != null && entitySprite?.Icon.Appearance?.Verbs is { } verbIds) { - foreach (var verbId in verbIds) { - if (!verbSystem.TryGetVerbInfo(verbId, out var verbInfo)) - continue; - if (verbInfo.IsHidden(false, seeInvisible)) + if (verbSystem != null) { + foreach (var verb in verbSystem.GetExecutableVerbs(_target)) { + if (verb.VerbInfo.IsHidden(false, seeInvisible)) continue; - AddVerb(verbId, verbInfo); + AddVerb(verb.Id, verb.Src, verb.VerbInfo); } } } - private void AddVerb(int verbId, VerbSystem.VerbInfo verbInfo) { - var netEntity = _entityManager.GetNetEntity(_entity); + private void AddVerb(int verbId, ClientObjectReference verbSrc, VerbSystem.VerbInfo verbInfo) { var button = new Button { Text = verbInfo.Name }; + var takesTargetArg = verbInfo.GetTargetType() != null && !verbSrc.Equals(_target); + button.OnPressed += _ => { - _verbSystem?.ExecuteVerb(new(netEntity), verbId); + _verbSystem?.ExecuteVerb(verbSrc, verbId, takesTargetArg ? [_target] : []); Close(); OnVerbSelected?.Invoke(); }; diff --git a/OpenDreamClient/Interface/Controls/ControlChild.cs b/OpenDreamClient/Interface/Controls/ControlChild.cs index 39c7ce5a2c..0f3b73ff44 100644 --- a/OpenDreamClient/Interface/Controls/ControlChild.cs +++ b/OpenDreamClient/Interface/Controls/ControlChild.cs @@ -4,11 +4,9 @@ namespace OpenDreamClient.Interface.Controls; +// todo: robust needs GridSplitter. +// and a non-shit grid control. internal sealed class ControlChild : InterfaceControl { - // todo: robust needs GridSplitter. - // and a non-shit grid control. - - private ControlDescriptorChild ChildDescriptor => (ControlDescriptorChild)ElementDescriptor; private SplitContainer _grid; diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 9667345e1c..ae51b1b402 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -26,10 +26,11 @@ internal sealed class DreamViewOverlay : Overlay { public bool ScreenOverlayEnabled = true; public bool MouseMapRenderEnabled; - public ShaderInstance BlockColorInstance; public Texture? MouseMap => _mouseMapRenderTarget?.Texture; + public readonly ShaderInstance BlockColorInstance; public readonly Dictionary MouseMapLookup = new(); public readonly Dictionary RenderSourceLookup = new(); + public readonly HashSet EntitiesInView = new(); private const LookupFlags MapLookupFlags = LookupFlags.Approximate | LookupFlags.Uncontained; @@ -74,7 +75,6 @@ internal sealed class DreamViewOverlay : Overlay { // Defined here so it isn't recreated every frame private ViewAlgorithm.Tile?[,]? _tileInfo; - private readonly HashSet _entities = new(); public DreamViewOverlay(TransformSystem transformSystem, MapSystem mapSystem, EntityLookupSystem lookupSystem, ClientAppearanceSystem appearanceSystem, ClientScreenOverlaySystem screenOverlaySystem, ClientImagesSystem clientImagesSystem) { @@ -150,15 +150,15 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) using (_prof.Group("lookup")) { //TODO use a sprite tree. //the scaling is to attempt to prevent pop-in, by rendering sprites that are *just* offscreen - _lookupSystem.GetEntitiesIntersecting(args.MapId, args.WorldAABB.Scale(1.2f), _entities, MapLookupFlags); + _lookupSystem.GetEntitiesIntersecting(args.MapId, args.WorldAABB.Scale(1.2f), EntitiesInView, MapLookupFlags); } var eyeTile = _mapSystem.GetTileRef(gridUid, grid, eyeCoords); - var tiles = CalculateTileVisibility(gridUid, grid, _entities, eyeTile, seeVis); + var tiles = CalculateTileVisibility(gridUid, grid, eyeTile, seeVis); RefreshRenderTargets(args.WorldHandle, viewportSize); - CollectVisibleSprites(tiles, gridUid, grid, eyeTile, _entities, seeVis, sight, args.WorldAABB); + CollectVisibleSprites(tiles, gridUid, grid, eyeTile, seeVis, sight, args.WorldAABB); ClearPlanes(); ProcessSprites(worldHandle, viewportSize, args.WorldAABB); @@ -576,7 +576,7 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { } } - private ViewAlgorithm.Tile?[,] CalculateTileVisibility(EntityUid gridUid, MapGridComponent grid, HashSet entities, TileRef eyeTile, int seeVis) { + private ViewAlgorithm.Tile?[,] CalculateTileVisibility(EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, int seeVis) { using var _ = _prof.Group("visible turfs"); var viewRange = _interfaceManager.View; @@ -613,7 +613,7 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { } // Apply entities' opacity - foreach (EntityUid entity in entities) { + foreach (EntityUid entity in EntitiesInView) { // TODO use a sprite tree. if (!_spriteQuery.TryGetComponent(entity, out var sprite)) continue; @@ -638,7 +638,7 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { return _tileInfo; } - private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, HashSet entities, int seeVis, SightFlags sight, Box2 worldAABB) { + private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, int seeVis, SightFlags sight, Box2 worldAABB) { _spriteContainer.Clear(); // This exists purely because the tiebreaker var needs to exist somewhere @@ -664,7 +664,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU // Visible entities using (var _ = _prof.Group("process entities")) { - foreach (EntityUid entity in entities) { + foreach (EntityUid entity in EntitiesInView) { // TODO use a sprite tree. if (!_spriteQuery.TryGetComponent(entity, out var sprite)) continue; diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 9765fdd8db..6b69fe4fc5 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -139,7 +139,7 @@ public void UpdateStat() { _currentlyUpdatingStat = true; _statPanels.Clear(); - DreamThread.Run("Stat", async (state) => { + DreamThread.Run("Stat", async state => { try { var statProc = Client.GetProc("Stat"); @@ -188,13 +188,8 @@ public void HandleMsgPromptResponse(MsgPromptResponse message) { return; } - DreamValue value = message.Type switch { - DreamValueType.Null => DreamValue.Null, - DreamValueType.Text or DreamValueType.Message => new DreamValue((string)message.Value), - DreamValueType.Num => new DreamValue((float)message.Value), - DreamValueType.Color => new DreamValue(((Color)message.Value).ToHexNoAlpha()), - _ => throw new Exception("Invalid prompt response '" + message.Type + "'") - }; + if (!TryConvertPromptResponse(message.Type, message.Value, out var value)) + throw new Exception($"Invalid prompt response '{value}'"); promptEvent.Invoke(value); _promptEvents.Remove(message.PromptId); @@ -472,4 +467,38 @@ public void SendFile(DreamResource file, string suggestedName) { Session?.ConnectedClient.SendMessage(msg); } + + public bool TryConvertPromptResponse(DreamValueType type, object? value, out DreamValue converted) { + if (type.HasFlag(DreamValueType.Null) && value == null) { + converted = DreamValue.Null; + return true; + } else if (type.HasFlag(DreamValueType.Text) || type.HasFlag(DreamValueType.Message)) { + if (value is string strVal) { + converted = new(strVal); + return true; + } + } else if (type.HasFlag(DreamValueType.Num) && value is float numVal) { + converted = new DreamValue(numVal); + return true; + } else if (type.HasFlag(DreamValueType.Color) && value is Color colorVal) { + converted = new DreamValue(colorVal.ToHexNoAlpha()); + return true; + } else if ((type & DreamValueType.AllAtomTypes) != 0x0 && value is ClientObjectReference clientRef) { + var atom = _dreamManager.GetFromClientReference(this, clientRef); + + if (atom != null) { + if ((atom.IsSubtypeOf(_objectTree.Obj) && !type.HasFlag(DreamValueType.Obj)) || + (atom.IsSubtypeOf(_objectTree.Mob) && !type.HasFlag(DreamValueType.Mob))) { + converted = default; + return false; + } + + converted = new(atom); + return true; + } + } + + converted = default; + return false; + } } diff --git a/OpenDreamRuntime/DreamThread.cs b/OpenDreamRuntime/DreamThread.cs index d79ed311a0..6b901d6374 100644 --- a/OpenDreamRuntime/DreamThread.cs +++ b/OpenDreamRuntime/DreamThread.cs @@ -34,12 +34,13 @@ public abstract class DreamProc { public int? VerbId = null; // Null until registered as a verb in ServerVerbSystem public string VerbName => _verbName ?? Name; public readonly string? VerbCategory = string.Empty; + public readonly VerbSrc? VerbSrc; public readonly sbyte Invisibility; private readonly string? _verbName; private readonly string? _verbDesc; - protected DreamProc(int id, TreeEntry owningType, string name, DreamProc? superProc, ProcAttributes attributes, List? argumentNames, List? argumentTypes, string? verbName, string? verbCategory, string? verbDesc, sbyte invisibility, bool isVerb = false) { + protected DreamProc(int id, TreeEntry owningType, string name, DreamProc? superProc, ProcAttributes attributes, List? argumentNames, List? argumentTypes, VerbSrc? verbSrc, string? verbName, string? verbCategory, string? verbDesc, sbyte invisibility, bool isVerb = false) { Id = id; OwningType = owningType; Name = name; @@ -49,6 +50,7 @@ protected DreamProc(int id, TreeEntry owningType, string name, DreamProc? superP ArgumentNames = argumentNames; ArgumentTypes = argumentTypes; + VerbSrc = verbSrc; _verbName = verbName; if (verbCategory is not null) { // (de)serialization meme to reduce JSON size diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index dd66697e71..ce86bb4c33 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -82,7 +82,7 @@ public void LoadJson(DreamCompiledJson json) { Strings = json.Strings ?? new(); if (json.GlobalInitProc is { } initProcDef) { - GlobalInitProc = new DMProc(0, Root, initProcDef, "", _dreamManager, _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler); + GlobalInitProc = new DMProc(0, Root, initProcDef, "", _dreamManager, _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler, _verbSystem); } else { GlobalInitProc = null; } @@ -346,7 +346,7 @@ private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? proc type.ParentEntry.ChildCount += type.ChildCount + 1; } - //Fifth pass: Set atom's name and text + // Fifth pass: Set atom's name and text foreach (TreeEntry type in GetAllDescendants(Atom)) { if (type.ObjectDefinition.Variables["name"].IsNull) type.ObjectDefinition.Variables["name"] = new(type.Name.Replace("_", " ")); @@ -355,6 +355,16 @@ private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? proc type.ObjectDefinition.Variables["text"] = new DreamValue(string.IsNullOrEmpty(name) ? string.Empty : name[..1]); } } + + // Register verbs + if (_verbSystem != null) { + foreach (DreamProc proc in Procs) { + if (!proc.IsVerb) + continue; + + _verbSystem.RegisterVerb(proc); + } + } } private void LoadVariablesFromJson(DreamObjectDefinition objectDefinition, DreamTypeJson jsonObject) { @@ -390,7 +400,7 @@ private void LoadVariablesFromJson(DreamObjectDefinition objectDefinition, Dream public DreamProc LoadProcJson(int id, ProcDefinitionJson procDefinition) { TreeEntry owningType = Types[procDefinition.OwningTypeId]; return new DMProc(id, owningType, procDefinition, null, _dreamManager, - _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler); + _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler, _verbSystem); } private void LoadProcsFromJson(ProcDefinitionJson[]? jsonProcs, int[]? jsonGlobalProcs) { @@ -401,10 +411,6 @@ private void LoadProcsFromJson(ProcDefinitionJson[]? jsonProcs, int[]? jsonGloba foreach (var procJson in jsonProcs) { var proc = LoadProcJson(Procs.Count, procJson); - if (proc.IsVerb) { - _verbSystem?.RegisterVerb(proc); - } - Procs.Add(proc); } } diff --git a/OpenDreamRuntime/Procs/AsyncNativeProc.cs b/OpenDreamRuntime/Procs/AsyncNativeProc.cs index 585206c5d2..53d7f275aa 100644 --- a/OpenDreamRuntime/Procs/AsyncNativeProc.cs +++ b/OpenDreamRuntime/Procs/AsyncNativeProc.cs @@ -169,7 +169,7 @@ public DreamValue GetArgument(int argumentPosition, string argumentName) { private readonly Func> _taskFunc; public AsyncNativeProc(int id, TreeEntry owningType, string name, List argumentNames, Dictionary defaultArgumentValues, Func> taskFunc) - : base(id, owningType, name, null, ProcAttributes.None, argumentNames, null, null, null, null, 0) { + : base(id, owningType, name, null, ProcAttributes.None, argumentNames, null, null, null, null, null, 0) { _defaultArgumentValues = defaultArgumentValues; _taskFunc = taskFunc; } diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index d43b5fae09..5148001246 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -170,37 +170,50 @@ public static ProcStatus CreateObject(DMProcState state) { if (!state.Proc.ObjectTree.TryGetTreeEntry(pathString, out objectType)) { ThrowCannotCreateUnknownObject(val); } + } else if (val.TryGetValueAsProc(out var proc)) { // new /proc/proc_name(Destination,Name,Desc) + var arguments = state.PopProcArguments(null, argumentInfo.Type, argumentInfo.StackSize); + var destination = arguments.GetArgument(0); + + // TODO: Name and Desc arguments + + if (destination.TryGetValueAsDreamObject(out var atom)) { + state.Proc.AtomManager.UpdateAppearance(atom, appearance => { + state.Proc.VerbSystem.RegisterVerb(proc); + + appearance.Verbs.Add(proc.VerbId!.Value); + }); + } else if (destination.TryGetValueAsDreamObject(out var client)) { + client.ClientVerbs.AddValue(val); + } + + return ProcStatus.Continue; } else { ThrowCannotCreateObjectFromInvalid(val); } } var objectDef = objectType.ObjectDefinition; - var proc = objectDef.GetProc("New"); - var arguments = state.PopProcArguments(proc, argumentInfo.Type, argumentInfo.StackSize); + var newProc = objectDef.GetProc("New"); + var newArguments = state.PopProcArguments(newProc, argumentInfo.Type, argumentInfo.StackSize); if (objectDef.IsSubtypeOf(state.Proc.ObjectTree.Turf)) { // Turfs are special. They're never created outside of map initialization // So instead this will replace an existing turf's type and return that same turf - DreamValue loc = arguments.GetArgument(0); + DreamValue loc = newArguments.GetArgument(0); if (!loc.TryGetValueAsDreamObject(out var turf)) ThrowInvalidTurfLoc(loc); - state.Proc.DreamMapManager.SetTurf(turf, objectDef, arguments); + state.Proc.DreamMapManager.SetTurf(turf, objectDef, newArguments); state.Push(loc); return ProcStatus.Continue; } - DreamObject newObject = state.Proc.ObjectTree.CreateObject(objectType); - var s = newObject.InitProc(state.Thread, state.Usr, arguments); - if (s is not null) { - state.Thread.PushProcState(s); - return ProcStatus.Called; - } + var newObject = state.Proc.ObjectTree.CreateObject(objectType); + var s = newObject.InitProc(state.Thread, state.Usr, newArguments); - state.Push(new DreamValue(newObject)); - return ProcStatus.Continue; + state.Thread.PushProcState(s); + return ProcStatus.Called; } [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index e42feb8ee9..cad89efe45 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -26,11 +26,12 @@ public sealed class DMProc : DreamProc { public readonly IDreamDebugManager DreamDebugManager; public readonly DreamResourceManager DreamResourceManager; public readonly DreamObjectTree ObjectTree; + public readonly ServerVerbSystem VerbSystem; private readonly int _maxStackSize; - public DMProc(int id, TreeEntry owningType, ProcDefinitionJson json, string? name, DreamManager dreamManager, AtomManager atomManager, IDreamMapManager dreamMapManager, IDreamDebugManager dreamDebugManager, DreamResourceManager dreamResourceManager, DreamObjectTree objectTree, ProcScheduler procScheduler) - : base(id, owningType, name ?? json.Name, null, json.Attributes, GetArgumentNames(json), GetArgumentTypes(json), json.VerbName, json.VerbCategory, json.VerbDesc, json.Invisibility, json.IsVerb) { + public DMProc(int id, TreeEntry owningType, ProcDefinitionJson json, string? name, DreamManager dreamManager, AtomManager atomManager, IDreamMapManager dreamMapManager, IDreamDebugManager dreamDebugManager, DreamResourceManager dreamResourceManager, DreamObjectTree objectTree, ProcScheduler procScheduler, ServerVerbSystem verbSystem) + : base(id, owningType, name ?? json.Name, null, json.Attributes, GetArgumentNames(json), GetArgumentTypes(json), json.VerbSrc, json.VerbName, json.VerbCategory, json.VerbDesc, json.Invisibility, json.IsVerb) { Bytecode = json.Bytecode ?? Array.Empty(); LocalNames = json.Locals; SourceInfo = json.SourceInfo; @@ -39,11 +40,12 @@ public DMProc(int id, TreeEntry owningType, ProcDefinitionJson json, string? nam AtomManager = atomManager; DreamManager = dreamManager; + ProcScheduler = procScheduler; DreamMapManager = dreamMapManager; DreamDebugManager = dreamDebugManager; DreamResourceManager = dreamResourceManager; ObjectTree = objectTree; - ProcScheduler = procScheduler; + VerbSystem = verbSystem; } public (string Source, int Line) GetSourceAtOffset(int offset) { diff --git a/OpenDreamRuntime/Procs/DreamProcArguments.cs b/OpenDreamRuntime/Procs/DreamProcArguments.cs index 86821c77eb..1d78d51c84 100644 --- a/OpenDreamRuntime/Procs/DreamProcArguments.cs +++ b/OpenDreamRuntime/Procs/DreamProcArguments.cs @@ -1,34 +1,34 @@ using System.Runtime.CompilerServices; -namespace OpenDreamRuntime.Procs { - public readonly ref struct DreamProcArguments { - public int Count => Values.Length; +namespace OpenDreamRuntime.Procs; - public readonly ReadOnlySpan Values; +public readonly ref struct DreamProcArguments { + public int Count => Values.Length; - public DreamProcArguments() { - Values = Array.Empty(); - } + public readonly ReadOnlySpan Values; - public DreamProcArguments(ReadOnlySpan values) { - Values = values; - } + public DreamProcArguments() { + Values = Array.Empty(); + } - public DreamProcArguments(params DreamValue[] values) { - Values = values; - } + public DreamProcArguments(ReadOnlySpan values) { + Values = values; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DreamValue GetArgument(int argumentPosition) { - if (Count > argumentPosition) { - return Values[argumentPosition]; - } + public DreamProcArguments(params DreamValue[] values) { + Values = values; + } - return DreamValue.Null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DreamValue GetArgument(int argumentPosition) { + if (Count > argumentPosition) { + return Values[argumentPosition]; } - public override string ToString() { - return $""; - } + return DreamValue.Null; + } + + public override string ToString() { + return $""; } } diff --git a/OpenDreamRuntime/Procs/NativeProc.cs b/OpenDreamRuntime/Procs/NativeProc.cs index e3333f5940..afe0314ab3 100644 --- a/OpenDreamRuntime/Procs/NativeProc.cs +++ b/OpenDreamRuntime/Procs/NativeProc.cs @@ -83,7 +83,7 @@ private DreamValue GetArgumentFallback(string argumentName) { private readonly delegate* _handler; public NativeProc(int id, TreeEntry owningType, string name, List argumentNames, Dictionary defaultArgumentValues, HandlerFn handler, DreamManager dreamManager, AtomManager atomManager, IDreamMapManager mapManager, DreamResourceManager resourceManager, WalkManager walkManager, DreamObjectTree objectTree) - : base(id, owningType, name, null, ProcAttributes.None, argumentNames, null, null, null, null, 0) { + : base(id, owningType, name, null, ProcAttributes.None, argumentNames, null, null, null, null, null, 0) { _defaultArgumentValues = defaultArgumentValues; _handler = (delegate*)handler.Method.MethodHandle.GetFunctionPointer(); diff --git a/OpenDreamRuntime/ServerVerbSystem.cs b/OpenDreamRuntime/ServerVerbSystem.cs index 27278257b8..c8f20560d1 100644 --- a/OpenDreamRuntime/ServerVerbSystem.cs +++ b/OpenDreamRuntime/ServerVerbSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; @@ -12,11 +12,14 @@ namespace OpenDreamRuntime; public sealed class ServerVerbSystem : VerbSystem { [Dependency] private readonly DreamManager _dreamManager = default!; [Dependency] private readonly AtomManager _atomManager = default!; + [Dependency] private readonly DreamObjectTree _objectTree = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly List _verbs = new(); private readonly Dictionary _verbIdToProc = new(); + private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.verbs"); + public override void Initialize() { _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; @@ -31,6 +34,51 @@ public void RegisterVerb(DreamProc verb) { if (verb.VerbId != null) // Verb has already been registered return; + var verbArguments = Array.Empty(); + if (verb.ArgumentTypes != null) { + verbArguments = new VerbArg[verb.ArgumentTypes.Count]; + + for (int i = 0; i < verb.ArgumentTypes.Count; i++) { + verbArguments[i] = new VerbArg { + Name = verb.ArgumentNames![i], + Types = verb.ArgumentTypes[i] + }; + } + } + + VerbAccessibility? verbAccessibility = verb.VerbSrc switch { + VerbSrc.View => VerbAccessibility.View, // TODO: Ranges on the view()/range() types + VerbSrc.InView => VerbAccessibility.InView, + VerbSrc.OView => VerbAccessibility.OView, + VerbSrc.InOView => VerbAccessibility.InOView, + VerbSrc.Range => VerbAccessibility.Range, + VerbSrc.InRange => VerbAccessibility.InRange, + VerbSrc.ORange => VerbAccessibility.ORange, + VerbSrc.InORange => VerbAccessibility.InORange, + VerbSrc.World => VerbAccessibility.InWorld, + VerbSrc.InWorld => VerbAccessibility.InWorld, + VerbSrc.Usr => VerbAccessibility.Usr, + VerbSrc.InUsr => VerbAccessibility.InUsr, + VerbSrc.UsrLoc => VerbAccessibility.UsrLoc, + VerbSrc.UsrGroup => VerbAccessibility.UsrGroup, + null => null, + _ => throw new UnreachableException("All cases should be covered") + }; + + if (verbAccessibility == null) { + var def = verb.OwningType.ObjectDefinition; + + // Assign a default based on the type this verb is defined on + if (def.IsSubtypeOf(_objectTree.Obj)) { + verbAccessibility = VerbAccessibility.InUsr; + } else if (def.IsSubtypeOf(_objectTree.Turf) || def.IsSubtypeOf(_objectTree.Area)) { + verbAccessibility = VerbAccessibility.View; // TODO: Range of 0 + } else { + // The default for everything else (/mob especially) + verbAccessibility = VerbAccessibility.Usr; + } + } + var verbInfo = new VerbInfo { Name = verb.VerbName, @@ -40,20 +88,11 @@ public void RegisterVerb(DreamProc verb) { Category = verb.VerbCategory ?? string.Empty, Invisibility = verb.Invisibility, - HiddenAttribute = (verb.Attributes & ProcAttributes.Hidden) == ProcAttributes.Hidden + HiddenAttribute = (verb.Attributes & ProcAttributes.Hidden) == ProcAttributes.Hidden, + Accessibility = verbAccessibility.Value, + Arguments = verbArguments }; - if (verb.ArgumentTypes != null) { - verbInfo.Arguments = new VerbArg[verb.ArgumentTypes.Count]; - - for (int i = 0; i < verb.ArgumentTypes.Count; i++) { - verbInfo.Arguments[i] = new VerbArg { - Name = verb.ArgumentNames![i], - Types = verb.ArgumentTypes[i] - }; - } - } - verb.VerbId = _verbs.Count; _verbs.Add(verbInfo); _verbIdToProc.Add(verb.VerbId.Value, verb); @@ -63,10 +102,6 @@ public void RegisterVerb(DreamProc verb) { public DreamProc GetVerb(int verbId) => _verbIdToProc[verbId]; - public bool TryGetVerb(int verbId, [NotNullWhen(true)] out DreamProc? verb) { - return _verbIdToProc.TryGetValue(verbId, out verb); - } - /// /// Send a client an updated version of its /client's verbs /// @@ -96,23 +131,24 @@ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { private void OnVerbExecuted(ExecuteVerbEvent msg, EntitySessionEventArgs args) { var connection = _dreamManager.GetConnectionBySession(args.SenderSession); var src = _dreamManager.GetFromClientReference(connection, msg.Src); - if (src == null || !TryGetVerb(msg.VerbId, out var verb) || !CanExecute(connection, src, verb)) + if (src == null || !_verbIdToProc.TryGetValue(msg.VerbId, out var verb) || !CanExecute(connection, src, verb)) return; - if (msg.Arguments.Length != verb.ArgumentTypes?.Count) + if (msg.Arguments.Length != verb.ArgumentTypes?.Count) { + _sawmill.Error( + $"User \"{args.SenderSession.Name}\" gave {msg.Arguments.Length} argument(s) to the \"{verb.Name}\" verb which only has {verb.ArgumentTypes?.Count} argument(s)"); return; + } // Convert the values the client gave to DreamValues DreamValue[] arguments = new DreamValue[verb.ArgumentTypes.Count]; for (int i = 0; i < verb.ArgumentTypes.Count; i++) { var argType = verb.ArgumentTypes[i]; - arguments[i] = argType switch { - DreamValueType.Null => DreamValue.Null, - DreamValueType.Text or DreamValueType.Message => new DreamValue((string)msg.Arguments[i]), - DreamValueType.Num => new DreamValue((float)msg.Arguments[i]), - DreamValueType.Color => new DreamValue(((Color)msg.Arguments[i]).ToHexNoAlpha()), - _ => throw new Exception("Invalid prompt response '" + msg.Arguments[i] + "'") - }; + if (!connection.TryConvertPromptResponse(argType, msg.Arguments[i], out arguments[i])) { + _sawmill.Error( + $"User \"{args.SenderSession.Name}\" gave an invalid value for argument #{i + 1} of verb \"{verb.Name}\""); + return; + } } DreamThread.Run($"Execute {msg.VerbId} by {connection.Session!.Name}", async state => { @@ -132,8 +168,13 @@ private bool CanExecute(DreamConnection connection, DreamObject src, DreamProc v if (verb.VerbId == null) // Not even a verb return false; - if (src is DreamObjectClient client && !client.ClientVerbs.Verbs.Contains(verb)) { // Inside client.verbs? - return false; + if (src is DreamObjectClient client) { + if (!client.ClientVerbs.Verbs.Contains(verb)) + return false; // Not inside client.verbs + + // Client verbs ignore "set src" checks + // Deviates from BYOND, where anything but usr and world shows the verb in the statpanel but is not executable + return true; } else if (src is DreamObjectAtom atom) { var appearance = _atomManager.MustGetAppearance(atom); @@ -141,7 +182,15 @@ private bool CanExecute(DreamConnection connection, DreamObject src, DreamProc v return false; } - // TODO: Does "set src = ..." allow execution here? - return true; + var verbInfo = _verbs[verb.VerbId.Value]; + + // Check that "set src = ..." allows execution in this instance + switch (verbInfo.Accessibility) { + case VerbAccessibility.Usr: + return src == connection.Mob; + default: + // TODO: All the other kinds + return true; + } } } diff --git a/OpenDreamShared/Dream/ClientObjectReference.cs b/OpenDreamShared/Dream/ClientObjectReference.cs index 3f38d74856..966073e3a6 100644 --- a/OpenDreamShared/Dream/ClientObjectReference.cs +++ b/OpenDreamShared/Dream/ClientObjectReference.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Maths; using Robust.Shared.Serialization; @@ -33,4 +34,29 @@ public ClientObjectReference(NetEntity entity) { Type = RefType.Entity; Entity = entity; } + + [Pure] + public bool Equals(ClientObjectReference other) { + if (Type != other.Type) + return false; + + switch (Type) { + case RefType.Client: + return true; + case RefType.Entity: + return Entity == other.Entity; + case RefType.Turf: + return TurfX == other.TurfX && TurfY == other.TurfY && TurfZ == other.TurfZ; + } + + return false; + } + + [Pure] + public bool Equals(ClientObjectReference? other) { + if (other == null) + return false; + + return Equals(other.Value); + } } diff --git a/OpenDreamShared/Dream/DreamValueType.cs b/OpenDreamShared/Dream/DreamValueType.cs index ff481d41e7..e79130bcb2 100644 --- a/OpenDreamShared/Dream/DreamValueType.cs +++ b/OpenDreamShared/Dream/DreamValueType.cs @@ -11,6 +11,8 @@ namespace OpenDreamShared.Dream; /// [Flags] public enum DreamValueType { + AllAtomTypes = Obj | Mob | Turf | Area, + Anything = 0x0, Null = 0x1, Text = 0x2, diff --git a/OpenDreamShared/Dream/VerbSystem.cs b/OpenDreamShared/Dream/VerbSystem.cs index 300feb84be..403fa391bf 100644 --- a/OpenDreamShared/Dream/VerbSystem.cs +++ b/OpenDreamShared/Dream/VerbSystem.cs @@ -36,6 +36,12 @@ public struct VerbInfo { /// public bool HiddenAttribute; + /// + /// Where the verb's src must be for the client to be able to execute it. + /// set src = usr + /// + public VerbAccessibility Accessibility; + /// /// The arguments of this verb /// @@ -46,13 +52,22 @@ public struct VerbInfo { public string GetCommandName() => Name.ToLowerInvariant().Replace(" ", "-"); // Case-insensitive, dashes instead of spaces + [Pure] public string GetCategoryOrDefault(string defaultCategory) => string.IsNullOrWhiteSpace(Category) ? defaultCategory : Category; // TODO: Hidden verbs probably shouldn't be sent to the client in the first place? + [Pure] public bool IsHidden(bool ignoreHiddenAttr, sbyte seeInvisibility) => (!ignoreHiddenAttr && (HiddenAttribute || Name.StartsWith('.'))) || seeInvisibility < Invisibility; + // If the verb's first argument is an atom type, it takes that type as a target + [Pure] + public DreamValueType? GetTargetType() => + (Arguments.Length != 0 && (Arguments[0].Types & DreamValueType.AllAtomTypes) != 0x0) + ? Arguments[0].Types + : null; + public override string ToString() => GetCommandName(); } @@ -69,6 +84,23 @@ public struct VerbArg { public DreamValueType Types; } + [Serializable, NetSerializable] + public enum VerbAccessibility : byte { + View, + InView, + OView, + InOView, + Range, + InRange, + ORange, + InORange, + InWorld, + Usr, + InUsr, + UsrLoc, + UsrGroup + } + [Serializable, NetSerializable] public sealed class AllVerbsEvent(List verbs) : EntityEventArgs { public List Verbs = verbs;