From 81a2a0470e07c90687f1dc1c81ea78b01a29d218 Mon Sep 17 00:00:00 2001 From: wixoa Date: Sat, 2 Dec 2023 12:43:08 -0500 Subject: [PATCH 01/64] Fix nullable warnings in DAP definitions (#1541) --- OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs | 3 +-- .../Procs/DebugAdapter/Protocol/Capabilities.cs | 2 ++ OpenDreamRuntime/Procs/DebugAdapter/Protocol/Checksum.cs | 6 ++++-- .../DebugAdapter/Protocol/DisassembledInstruction.cs | 4 ++-- .../Procs/DebugAdapter/Protocol/ExceptionBreakMode.cs | 8 -------- .../Procs/DebugAdapter/Protocol/ExceptionDetails.cs | 2 ++ .../Procs/DebugAdapter/Protocol/FunctionBreakpoint.cs | 4 +++- .../Procs/DebugAdapter/Protocol/ProtocolMessage.cs | 4 +--- OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs | 4 ++-- .../DebugAdapter/Protocol/RequestConfigurationDone.cs | 3 +++ .../Procs/DebugAdapter/Protocol/RequestContinue.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestDisassemble.cs | 7 +++++-- .../Procs/DebugAdapter/Protocol/RequestDisconnect.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestExceptionInfo.cs | 9 ++++++--- .../Procs/DebugAdapter/Protocol/RequestInitialize.cs | 7 +++++-- .../Procs/DebugAdapter/Protocol/RequestLaunch.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestNext.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestPause.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestScopes.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestSetBreakpoints.cs | 7 +++++-- .../Protocol/RequestSetExceptionBreakpoints.cs | 9 +++++++-- .../Protocol/RequestSetFunctionBreakpoints.cs | 7 +++++-- .../Procs/DebugAdapter/Protocol/RequestStackTrace.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestStepIn.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestStepOut.cs | 5 ++++- .../Procs/DebugAdapter/Protocol/RequestThreads.cs | 3 +++ .../Procs/DebugAdapter/Protocol/RequestVariables.cs | 5 ++++- OpenDreamRuntime/Procs/DebugAdapter/Protocol/Scope.cs | 2 +- .../Procs/DebugAdapter/Protocol/SourceBreakpoint.cs | 2 ++ .../Procs/DebugAdapter/Protocol/StackFrame.cs | 2 +- .../Procs/DebugAdapter/Protocol/StackFrameFormat.cs | 2 ++ .../Procs/DebugAdapter/Protocol/StoppedEvent.cs | 2 +- OpenDreamRuntime/Procs/DebugAdapter/Protocol/Variable.cs | 4 ++-- .../DebugAdapter/Protocol/VariablePresentationHint.cs | 2 ++ 34 files changed, 107 insertions(+), 48 deletions(-) delete mode 100644 OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionBreakMode.cs diff --git a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs index 9ddd53599b..be15bd06db 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs @@ -720,8 +720,7 @@ private IEnumerable ExpandStack(RequestVariables req, ReadOnlyMemory 0) { varDesc.VariablesReference = AllocVariableRef(req => ExpandList(req, list)); diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Capabilities.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Capabilities.cs index ede0a06ab6..b5205946e2 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Capabilities.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Capabilities.cs @@ -78,11 +78,13 @@ public sealed class Capabilities { */ [JsonPropertyName("supportsModulesRequest")] public bool? SupportsModulesRequest { get; set; } + /* /** * The set of additional module information exposed by the debug adapter. */ //[JsonPropertyName("additionalModuleColumns")] public ColumnDescriptor[]? AdditionalModuleColumns { get; set; } + /* /** * Checksum algorithms supported by the debug adapter. */ diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Checksum.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Checksum.cs index 04aff0dbeb..dcd7fbad84 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Checksum.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Checksum.cs @@ -1,8 +1,10 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class Checksum { - [JsonPropertyName("algorithm")] public string Algorithm { get; set; } - [JsonPropertyName("checksum")] public string ChecksumValue { get; set; } + [JsonPropertyName("algorithm")] public required string Algorithm { get; set; } + [JsonPropertyName("checksum")] public required string ChecksumValue { get; set; } } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/DisassembledInstruction.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/DisassembledInstruction.cs index c2df10a639..65d338cd2e 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/DisassembledInstruction.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/DisassembledInstruction.cs @@ -7,7 +7,7 @@ public sealed class DisassembledInstruction { * The address of the instruction. Treated as a hex value if prefixed with * `0x`, or as a decimal value otherwise. */ - [JsonPropertyName("address")] public string Address { get; set; } + [JsonPropertyName("address")] public required string Address { get; set; } /** * Raw bytes representing the instruction and its operands, in an @@ -19,7 +19,7 @@ public sealed class DisassembledInstruction { * Text representing the instruction and its operands, in an * implementation-defined format. */ - [JsonPropertyName("instruction")] public string Instruction { get; set; } + [JsonPropertyName("instruction")] public required string Instruction { get; set; } /** * Name of the symbol that corresponds with the location of this instruction, diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionBreakMode.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionBreakMode.cs deleted file mode 100644 index e50bfc92d2..0000000000 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionBreakMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; - -public enum ExceptionBreakMode { - Never, - Always, - Unhandled, - UserUnhandled, -} diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionDetails.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionDetails.cs index 2cc9c01fc2..4ac648f0b8 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionDetails.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ExceptionDetails.cs @@ -1,7 +1,9 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class ExceptionDetails { /** * Message contained in the exception. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/FunctionBreakpoint.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/FunctionBreakpoint.cs index 970a30cc24..05ff40842a 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/FunctionBreakpoint.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/FunctionBreakpoint.cs @@ -1,9 +1,11 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class FunctionBreakpoint { - [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("name")] public required string Name { get; set; } [JsonPropertyName("condition")] public string? Condition { get; set; } [JsonPropertyName("hitCondition")] public string? HitCondition { get; set; } } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs index c53126d556..6404e3723a 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs @@ -5,9 +5,7 @@ namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; [Virtual] public class ProtocolMessage { [JsonPropertyName("seq")] public int Seq { get; set; } - [JsonPropertyName("type")] public string Type { get; set; } - - public ProtocolMessage() { } + [JsonPropertyName("type")] public string Type { get; } protected ProtocolMessage(string type) { Type = type; diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs index d1d187a4e3..5259cb1127 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs @@ -5,9 +5,9 @@ namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; [Virtual] public class Request : ProtocolMessage { - [JsonPropertyName("command")] public string Command { get; set; } + [JsonPropertyName("command")] public required string Command { get; set; } - public Request() : base("request") { } + protected Request() : base("request") { } public static Request? DeserializeRequest(JsonDocument json) { Request? request = json.Deserialize(); diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestConfigurationDone.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestConfigurationDone.cs index 056d31f4e6..a7f2ed8d09 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestConfigurationDone.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestConfigurationDone.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestConfigurationDone : Request { public void Respond(DebugAdapterClient client) { client.SendMessage(Response.NewSuccess(this)); diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestContinue.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestContinue.cs index c294b838f2..49f911ce6c 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestContinue.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestContinue.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestContinue : Request { - [JsonPropertyName("arguments")] public RequestContinueArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestContinueArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestContinueArguments { /** * Specifies the active thread. If the debug adapter supports single thread diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisassemble.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisassemble.cs index f3105b209f..4b54ec9678 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisassemble.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisassemble.cs @@ -1,16 +1,19 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestDisassemble : Request { - [JsonPropertyName("arguments")] public RequestDisassembleArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestDisassembleArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestDisassembleArguments { /** * Memory reference to the base location containing the instructions to * disassemble. */ - [JsonPropertyName("memoryReference")] public string MemoryReference { get; set; } + [JsonPropertyName("memoryReference")] public required string MemoryReference { get; set; } /** * Offset (in bytes) to be applied to the reference location before diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisconnect.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisconnect.cs index 613b1f1149..0e5f10e2df 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisconnect.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestDisconnect.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestDisconnect : Request { - [JsonPropertyName("arguments")] public RequestDisconnectArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestDisconnectArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestDisconnectArguments { [JsonPropertyName("restart")] public bool Restart { get; set; } [JsonPropertyName("terminateDebuggee")] public bool TerminateDebuggee { get; set; } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestExceptionInfo.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestExceptionInfo.cs index b9c0f2c320..72fd1c48e2 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestExceptionInfo.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestExceptionInfo.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestExceptionInfo : Request { - [JsonPropertyName("arguments")] public RequestExceptionInfoArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestExceptionInfoArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestExceptionInfoArguments { /** * Thread for which exception information should be retrieved. @@ -16,7 +19,7 @@ public sealed class ExceptionInfoResponse { /** * ID of the exception that was thrown. */ - [JsonPropertyName("exceptionId")] public string ExceptionId { get; set; } + [JsonPropertyName("exceptionId")] public required string ExceptionId { get; set; } /** * Descriptive text for the exception. @@ -26,7 +29,7 @@ public sealed class ExceptionInfoResponse { /** * Mode that caused the exception notification to be raised. */ - [JsonPropertyName("breakMode")] public string BreakMode { get; set; } + [JsonPropertyName("breakMode")] public required string BreakMode { get; set; } // enum ExceptionBreakMode, no custom values public const string BreakModeNever = "never"; public const string BreakModeAlways = "always"; diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestInitialize.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestInitialize.cs index f93658def6..08b4c631fa 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestInitialize.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestInitialize.cs @@ -1,14 +1,17 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestInitialize : Request { - [JsonPropertyName("arguments")] public RequestInitializeArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestInitializeArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestInitializeArguments { [JsonPropertyName("clientID")] public string? ClientId { get; set; } [JsonPropertyName("clientName")] public string? ClientName { get; set; } - [JsonPropertyName("adapterID")] public string AdapterId { get; set; } + [JsonPropertyName("adapterID")] public required string AdapterId { get; set; } [JsonPropertyName("locale")] public string? Locale { get; set; } [JsonPropertyName("linesStartAt1")] public bool LinesStartAt1 { get; set; } = true; [JsonPropertyName("columnsStartAt1")] public bool ColumnsStartAt1 { get; set; } = true; diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestLaunch.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestLaunch.cs index 6fec653e37..93c2dfb531 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestLaunch.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestLaunch.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestLaunch : Request { - [JsonPropertyName("arguments")] public RequestLaunchArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestLaunchArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestLaunchArguments { [JsonPropertyName("noDebug")] public bool NoDebug { get; set; } [JsonPropertyName("__restart")] public object? RestartData { get; set; } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestNext.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestNext.cs index 53052ce106..89a3e452b2 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestNext.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestNext.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestNext : Request { - [JsonPropertyName("arguments")] public RequestNextArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestNextArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestNextArguments { /** * Specifies the thread for which to resume execution for one step (of the diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestPause.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestPause.cs index 61530996fb..1ece714f0c 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestPause.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestPause.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestPause : Request { - [JsonPropertyName("arguments")] public RequestPauseArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestPauseArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestPauseArguments { /** * Pause execution for this thread. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestScopes.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestScopes.cs index c6f38f37b4..c981520c45 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestScopes.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestScopes.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestScopes : Request { - [JsonPropertyName("arguments")] public RequestScopesArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestScopesArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestScopesArguments { /** * Retrieve the scopes for this stackframe. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetBreakpoints.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetBreakpoints.cs index f4436a31e7..cc4bb7a516 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetBreakpoints.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetBreakpoints.cs @@ -1,12 +1,15 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestSetBreakpoints : Request { - [JsonPropertyName("arguments")] public RequestSetBreakpointsArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestSetBreakpointsArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestSetBreakpointsArguments { - [JsonPropertyName("source")] public Source Source { get; set; } + [JsonPropertyName("source")] public required Source Source { get; set; } [JsonPropertyName("breakpoints")] public SourceBreakpoint[]? Breakpoints { get; set; } [JsonPropertyName("sourceModified")] public bool SourceModified { get; set; } } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetExceptionBreakpoints.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetExceptionBreakpoints.cs index 99d3dafb5d..7634929a4f 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetExceptionBreakpoints.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetExceptionBreakpoints.cs @@ -1,18 +1,22 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestSetExceptionBreakpoints : Request { - [JsonPropertyName("arguments")] public RequestSetExceptionBreakpointsArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestSetExceptionBreakpointsArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestSetExceptionBreakpointsArguments { /** * Set of exception filters specified by their ID. The set of all possible * exception filters is defined by the `exceptionBreakpointFilters` * capability. The `filter` and `filterOptions` sets are additive. */ - [JsonPropertyName("filters")] public string[] Filters { get; set; } + [JsonPropertyName("filters")] public required string[] Filters { get; set; } + /* /** * Set of exception filters and their options. The set of all possible * exception filters is defined by the `exceptionBreakpointFilters` @@ -22,6 +26,7 @@ public sealed class RequestSetExceptionBreakpointsArguments { */ //[JsonPropertyName("filterOptions")] public ExceptionFilterOptions[]? FilterOptions { get; set; } + /* /** * Configuration options for selected exceptions. * The attribute is only honored by a debug adapter if the corresponding diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetFunctionBreakpoints.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetFunctionBreakpoints.cs index 930baf5547..3a74626d9f 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetFunctionBreakpoints.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestSetFunctionBreakpoints.cs @@ -1,12 +1,15 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestSetFunctionBreakpoints : Request { - [JsonPropertyName("arguments")] public RequestSetBreakpointsArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestSetBreakpointsArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestSetBreakpointsArguments { - [JsonPropertyName("breakpoints")] public FunctionBreakpoint[] Breakpoints { get; set; } + [JsonPropertyName("breakpoints")] public required FunctionBreakpoint[] Breakpoints { get; set; } } public void Respond(DebugAdapterClient client, Breakpoint[] breakpoints) { diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStackTrace.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStackTrace.cs index 995fef7e52..b1e429d7a3 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStackTrace.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStackTrace.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestStackTrace : Request { - [JsonPropertyName("arguments")] public RequestSetBreakpointsArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestSetBreakpointsArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestSetBreakpointsArguments { /** * Retrieve the stacktrace for this thread. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepIn.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepIn.cs index e10e0cca7c..afb8a63fc2 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepIn.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepIn.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestStepIn : Request { - [JsonPropertyName("arguments")] public RequestStepInArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestStepInArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestStepInArguments { /** * Specifies the thread for which to resume execution for one step-into (of diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepOut.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepOut.cs index a7e7ba0ec6..5b42d5a538 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepOut.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestStepOut.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestStepOut : Request { - [JsonPropertyName("arguments")] public RequestStepOutArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestStepOutArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestStepOutArguments { /** * Specifies the thread for which to resume execution for one step (of the diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestThreads.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestThreads.cs index d9a32e9246..25514f1a92 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestThreads.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestThreads.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestThreads : Request { public void Respond(DebugAdapterClient client, IEnumerable threads) { client.SendMessage(Response.NewSuccess(this, new { threads })); diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestVariables.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestVariables.cs index 66a47a3d14..37913f8cad 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestVariables.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/RequestVariables.cs @@ -1,10 +1,13 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class RequestVariables : Request { - [JsonPropertyName("arguments")] public RequestVariablesArguments Arguments { get; set; } + [JsonPropertyName("arguments")] public required RequestVariablesArguments Arguments { get; set; } + [UsedImplicitly] public sealed class RequestVariablesArguments { /** * The Variable reference. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Scope.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Scope.cs index 4c120a815a..7215b049be 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Scope.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Scope.cs @@ -7,7 +7,7 @@ public sealed class Scope { * Name of the scope such as 'Arguments', 'Locals', or 'Registers'. This * string is shown in the UI as is and can be translated. */ - [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("name")] public required string Name { get; set; } /** * A hint for how to present this scope in the UI. If this attribute is diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/SourceBreakpoint.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/SourceBreakpoint.cs index 44716ae4e6..33df827237 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/SourceBreakpoint.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/SourceBreakpoint.cs @@ -1,7 +1,9 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class SourceBreakpoint { [JsonPropertyName("line")] public int Line { get; set; } [JsonPropertyName("column")] public int? Column { get; set; } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrame.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrame.cs index e8d160544f..bffc36a82e 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrame.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrame.cs @@ -13,7 +13,7 @@ public sealed class StackFrame { /** * The name of the stack frame, typically a method name. */ - [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("name")] public required string Name { get; set; } /** * The source of the frame. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrameFormat.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrameFormat.cs index 58004c1710..1b3d52383a 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrameFormat.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StackFrameFormat.cs @@ -1,7 +1,9 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class StackFrameFormat : ValueFormat { /** * Displays parameters for the stack frame. diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StoppedEvent.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StoppedEvent.cs index c531325997..e618c62fc1 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StoppedEvent.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/StoppedEvent.cs @@ -12,7 +12,7 @@ public sealed class StoppedEvent : IEvent { * Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', * 'function breakpoint', 'data breakpoint', 'instruction breakpoint', etc. */ - [JsonPropertyName("reason")] public string Reason { get; set; } + [JsonPropertyName("reason")] public required string Reason { get; set; } public const string ReasonStep = "step"; public const string ReasonBreakpoint = "breakpoint"; public const string ReasonException = "exception"; diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Variable.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Variable.cs index dfa02fac0d..ad0cf527b3 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Variable.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Variable.cs @@ -6,7 +6,7 @@ public sealed class Variable { /** * The variable's name. */ - [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("name")] public required string Name { get; set; } /** * The variable's value. @@ -17,7 +17,7 @@ public sealed class Variable { * its children are not yet visible. * An empty string can be used if no value should be shown in the UI. */ - [JsonPropertyName("value")] public string Value { get; set; } + [JsonPropertyName("value")] public required string Value { get; set; } /** * The type of the variable's value. Typically shown in the UI when hovering diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/VariablePresentationHint.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/VariablePresentationHint.cs index ecbc4f38fe..f4c950b3c0 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/VariablePresentationHint.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/VariablePresentationHint.cs @@ -1,7 +1,9 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; +[UsedImplicitly] public sealed class VariablePresentationHint { /** * The kind of variable. Before introducing additional values, try to use the From f8f581df1d25daebb2ac842a350c425e01ed775b Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 5 Dec 2023 23:47:48 -0500 Subject: [PATCH 02/64] Fix getting an appearance's `appearance` var (#1542) Gives a copy of the appearance --- OpenDreamRuntime/AtomManager.cs | 6 ++++++ OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs | 9 +++++++++ OpenDreamRuntime/Objects/Types/DreamObjectImage.cs | 10 ++-------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index ebd76cfdb8..c5bb012ac7 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -232,6 +232,7 @@ public bool IsValidAppearanceVar(string name) { case "render_source": case "render_target": case "transform": + case "appearance": return true; // Get/SetAppearanceVar doesn't handle these @@ -336,6 +337,8 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal appearance.Transform = transformArray; break; + case "appearance": + throw new Exception("Cannot assign the appearance var on an appearance"); // TODO: overlays, underlays, filters // Those are handled separately by whatever is calling SetAppearanceVar currently default: @@ -409,6 +412,9 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { transform[1], transform[3], transform[5]); return new(matrix); + case "appearance": + IconAppearance appearanceCopy = new IconAppearance(appearance); // Return a copy + return new(appearanceCopy); // TODO: overlays, underlays, filters // Those are handled separately by whatever is calling GetAppearanceVar currently default: diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index b7c84ef30e..988be375f5 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -102,6 +102,15 @@ protected override void SetVar(string varName, DreamValue value) { case "desc": value.TryGetValueAsString(out Desc); break; + case "appearance": + if (!AtomManager.TryCreateAppearanceFrom(value, out var newAppearance)) + return; // Ignore attempts to set an invalid appearance + + // The dir does not get changed + newAppearance.Direction = AtomManager.MustGetAppearance(this)!.Direction; + + AtomManager.SetAtomAppearance(this, newAppearance); + break; case "overlays": { Overlays.Cut(); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs index 95196862cb..1c554b254a 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs @@ -68,11 +68,6 @@ public override void Initialize(DreamProcArguments args) { protected override bool TryGetVar(string varName, out DreamValue value) { // TODO: filters, transform switch(varName) { - case "appearance": { - IconAppearance appearanceCopy = new IconAppearance(Appearance!); // Return a copy - value = new(appearanceCopy); - return true; - } case "loc": { value = new(_loc); return true; @@ -97,13 +92,12 @@ protected override bool TryGetVar(string varName, out DreamValue value) { protected override void SetVar(string varName, DreamValue value) { switch (varName) { - case "appearance": + case "appearance": // Appearance var is mutable, don't use AtomManager.SetAppearanceVar() if (!AtomManager.TryCreateAppearanceFrom(value, out var newAppearance)) return; // Ignore attempts to set an invalid appearance // The dir does not get changed - var oldDir = Appearance!.Direction; - newAppearance.Direction = oldDir; + newAppearance.Direction = Appearance!.Direction; Appearance = newAppearance; break; From 8b5f5a2efcb6219fadc6597e935ecae8809597f1 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Wed, 6 Dec 2023 04:56:18 +0000 Subject: [PATCH 03/64] BlendMode Default Parent Inheritence (#1543) * duh * Delete TestGame/.vscode/launch.json --- OpenDreamClient/Rendering/DreamViewOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index ce6dd1c04e..ab2f4a514b 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -219,6 +219,9 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u current.Layer = parentIcon.Layer; else current.Layer = icon.Appearance.Layer; + + if (current.BlendMode == BlendMode.Default) + current.BlendMode = parentIcon.BlendMode; } else { current.ColorToApply = icon.Appearance.Color; current.ColorMatrixToApply = icon.Appearance.ColorMatrix; From 4236e9640471782765aa176c7b1f8f80569175ad Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 6 Dec 2023 23:54:29 -0500 Subject: [PATCH 04/64] Fix the debugger (#1548) --- .../Procs/DebugAdapter/Protocol/ProtocolMessage.cs | 6 +++++- OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs index 6404e3723a..41044230e6 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/ProtocolMessage.cs @@ -1,11 +1,15 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; [Virtual] public class ProtocolMessage { [JsonPropertyName("seq")] public int Seq { get; set; } - [JsonPropertyName("type")] public string Type { get; } + [JsonPropertyName("type")] public string Type { get; set; } = null!; + + [UsedImplicitly] + public ProtocolMessage() { } protected ProtocolMessage(string type) { Type = type; diff --git a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs index 5259cb1127..4bf3f2917c 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/Protocol/Request.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; @@ -7,7 +8,8 @@ namespace OpenDreamRuntime.Procs.DebugAdapter.Protocol; public class Request : ProtocolMessage { [JsonPropertyName("command")] public required string Command { get; set; } - protected Request() : base("request") { } + [UsedImplicitly] + public Request() : base("request") { } public static Request? DeserializeRequest(JsonDocument json) { Request? request = json.Deserialize(); From 1767929536f77a10c34f11ba97a97f2f5c6b7e87 Mon Sep 17 00:00:00 2001 From: Letter N <24603524+LetterN@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:06:53 +0800 Subject: [PATCH 05/64] Add --help for DMCompiler (#1547) * a * cosmetic change * wiki link * update help text --- DMCompiler/Program.cs | 282 ++++++++++++++++++++++-------------------- 1 file changed, 151 insertions(+), 131 deletions(-) diff --git a/DMCompiler/Program.cs b/DMCompiler/Program.cs index 8cbd5c0a3e..4c89e178f5 100644 --- a/DMCompiler/Program.cs +++ b/DMCompiler/Program.cs @@ -4,163 +4,183 @@ using System.Linq; using Robust.Shared.Utility; -namespace DMCompiler { - internal struct Argument { - /// The text we found that's in the '--whatever' format. May be null if no such text was present. - public string? Name; - /// The value, either set in a '--whatever=whoever' format or just left by itself anonymously. May be null. - public string? Value; - } +namespace DMCompiler; - internal static class Program { - private static void Main(string[] args) { - if (!TryParseArguments(args, out DMCompilerSettings settings)) { - Environment.Exit(1); - return; - } +internal struct Argument { + /// The text we found that's in the '--whatever' format. May be null if no such text was present. + public string? Name; + /// The value, either set in a '--whatever=whoever' format or just left by itself anonymously. May be null. + public string? Value; +} - if (!DMCompiler.Compile(settings)) { - //Compile errors, exit with an error code - Environment.Exit(1); - } +internal static class Program { + private static void Main(string[] args) { + if (!TryParseArguments(args, out DMCompilerSettings settings)) { + Environment.Exit(1); + return; } - /// Helper for TryParseArguments(), to turn the arg array into something better-parsed. - private static IEnumerable StringArrayToArguments(string[] args) { - List retArgs = new(args.Length); - for(var i = 0; i < args.Length;i+=1) { - string firstString = args[i]; - if(string.IsNullOrWhiteSpace(firstString)) // Is this possible? I don't even know. (IsNullOrWhiteSpace also checks if the string is empty, btw) - continue; - if(!firstString.StartsWith("--")) { // If it's a value-only argument - retArgs.Add(new Argument { Value = firstString }); - continue; - } - firstString = firstString.TrimStart('-'); - var split = firstString.Split('='); - if(split.Length == 1) { // If it's a name-only argument - if(firstString == "define" && i + 1 < args.Length) { // Weird snowflaking to make our define syntax work - i+=1; - if(!args[i].StartsWith("--")) { // To make the error make a schmidge more sense - retArgs.Add(new Argument {Name = firstString, Value = args[i] }); - } + if (!DMCompiler.Compile(settings)) { + //Compile errors, exit with an error code + Environment.Exit(1); + } + } + + /// Helper for TryParseArguments(), to turn the arg array into something better-parsed. + private static IEnumerable StringArrayToArguments(string[] args) { + List retArgs = new(args.Length); + for(var i = 0; i < args.Length;i+=1) { + var firstString = args[i]; + if(string.IsNullOrWhiteSpace(firstString)) // Is this possible? I don't even know. (IsNullOrWhiteSpace also checks if the string is empty, btw) + continue; + if(!firstString.StartsWith("--")) { // If it's a value-only argument + retArgs.Add(new Argument { Value = firstString }); + continue; + } + firstString = firstString.TrimStart('-'); + var split = firstString.Split('='); + if(split.Length == 1) { // If it's a name-only argument + if(firstString == "define" && i + 1 < args.Length) { // Weird snowflaking to make our define syntax work + i+=1; + if(!args[i].StartsWith("--")) { // To make the error make a schmidge more sense + retArgs.Add(new Argument {Name = firstString, Value = args[i] }); } - retArgs.Add(new Argument { Name = firstString }); - continue; } - retArgs.Add(new Argument { Name = split[0], Value = split[1] }); + retArgs.Add(new Argument { Name = firstString }); + continue; } - - return retArgs.AsEnumerable(); + retArgs.Add(new Argument { Name = split[0], Value = split[1] }); } - private static bool HasValidDMExtension(string filename) { - string extension = Path.GetExtension(filename); - return !String.IsNullOrEmpty(extension) && (extension == ".dme" || extension == ".dm"); - } + return retArgs.AsEnumerable(); + } - private static bool TryParseArguments(string[] args, out DMCompilerSettings settings) { - settings = new(); - settings.Files = new List(); - - bool skipBad = args.Contains("--skip-bad-args"); - - foreach (Argument arg in StringArrayToArguments(args)) { - switch (arg.Name) { - case "suppress-unimplemented": settings.SuppressUnimplementedWarnings = true; break; - case "dump-preprocessor": settings.DumpPreprocessor = true; break; - case "no-standard": settings.NoStandard = true; break; - case "verbose": settings.Verbose = true; break; - case "skip-bad-args": break; - case "define": - string[] parts = arg.Value.Split('=', 2); // Only split on the first = in case of stuff like "--define AAA=0==1" - if (parts.Length == 0) { - Console.WriteLine("Compiler arg 'define' requires macro identifier for definition directive"); - return false; - } - DebugTools.Assert(parts.Length <= 2); - settings.MacroDefines ??= new(); - settings.MacroDefines[parts[0]] = parts.Length > 1 ? parts[1] : ""; - break; - case "wall": - case "notices-enabled": - settings.NoticesEnabled = true; - break; - case "pragma-config": { - if(arg.Value is null || !HasValidDMExtension(arg.Value)) { - if(skipBad) { - DMCompiler.ForcedWarning($"Compiler arg 'pragma-config' requires filename of valid DM file, skipping"); - continue; - } - Console.WriteLine("Compiler arg 'pragma-config' requires filename of valid DM file"); - return false; - } - settings.PragmaFileOverride = arg.Value; - break; - } - case "version": { - if(arg.Value is null) { - if(skipBad) { - DMCompiler.ForcedWarning("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584), skipping"); - continue; - } - Console.WriteLine("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584)"); - return false; - } + private static bool HasValidDMExtension(string filename) { + var extension = Path.GetExtension(filename); + return !string.IsNullOrEmpty(extension) && (extension == ".dme" || extension == ".dm"); + } - var split = arg.Value.Split('.', StringSplitOptions.RemoveEmptyEntries); - if (split.Length != 2 || !int.TryParse(split[0], out _) || !int.TryParse(split[1], out _)) { // We want to make sure that they *are* ints but the preprocessor takes strings - if(skipBad) { - DMCompiler.ForcedWarning("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584), skipping"); - continue; - } - Console.WriteLine("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584)"); - return false; - } + private static void PrintHelp() { + Console.WriteLine("DM Compiler for OpenDream"); + Console.WriteLine("For more information please visit https://github.com/OpenDreamProject/OpenDream/wiki"); + Console.WriteLine("Usage: ./DMCompiler [options] [file].dme\n"); + Console.WriteLine("Options and arguments:"); + Console.WriteLine("--help : Show this help"); + Console.WriteLine("--version [VER].[BUILD] : Used to set the DM_VERSION and DM_BUILD macros"); + Console.WriteLine("--skip-bad-args : Skip arguments the compiler doesn't recognize"); + Console.WriteLine("--suppress-unimplemented : Do not warn about unimplemented proc and var uses"); + Console.WriteLine("--dump-preprocessor : This saves the result of preprocessing (#include, #if, defines, etc) in a file called preprocessor_dump.dm beside the given DME file."); + Console.WriteLine("--no-standard : This disables objects and procs that are usually built-into every DM program by not including DMStandard.dm."); + Console.WriteLine("--define [KEY=VAL] : Add extra defines to the compilation"); + Console.WriteLine("--verbose : Show verbose output during compile"); + Console.WriteLine("--notices-enabled : Show notice output during compile"); + Console.WriteLine("--pragma-config [file].dm : Configure the error/warning/notice/ignore level of compiler messages"); + } - settings.DMVersion = split[0]; - settings.DMBuild = split[1]; - break; + private static bool TryParseArguments(string[] args, out DMCompilerSettings settings) { + settings = new DMCompilerSettings { + Files = new List() + }; + + var skipBad = args.Contains("--skip-bad-args"); + + foreach (Argument arg in StringArrayToArguments(args)) { + switch (arg.Name) { + case "help": + PrintHelp(); + return false; + case "suppress-unimplemented": settings.SuppressUnimplementedWarnings = true; break; + case "dump-preprocessor": settings.DumpPreprocessor = true; break; + case "no-standard": settings.NoStandard = true; break; + case "verbose": settings.Verbose = true; break; + case "skip-bad-args": break; + case "define": + var parts = arg.Value?.Split('=', 2); // Only split on the first = in case of stuff like "--define AAA=0==1" + if (parts is { Length: 0 }) { + Console.WriteLine("Compiler arg 'define' requires macro identifier for definition directive"); + return false; } - case null: { // Value-only argument - if (arg.Value is null) // A completely empty argument? This should be a bug. + DebugTools.Assert(parts is { Length: <= 2 }); + settings.MacroDefines ??= new Dictionary(); + settings.MacroDefines[parts[0]] = parts.Length > 1 ? parts[1] : ""; + break; + case "wall": + case "notices-enabled": + settings.NoticesEnabled = true; + break; + case "pragma-config": { + if(arg.Value is null || !HasValidDMExtension(arg.Value)) { + if(skipBad) { + DMCompiler.ForcedWarning($"Compiler arg 'pragma-config' requires filename of valid DM file, skipping"); continue; - if (HasValidDMExtension(arg.Value)) { - settings.Files.Add(arg.Value); - break; } + Console.WriteLine("Compiler arg 'pragma-config' requires filename of valid DM file"); + return false; + } + settings.PragmaFileOverride = arg.Value; + break; + } + case "version": { + if(arg.Value is null) { if(skipBad) { - DMCompiler.ForcedWarning($"Invalid compiler arg '{arg.Value}', skipping"); - } else { - Console.WriteLine($"Invalid arg '{arg}'"); - return false; + DMCompiler.ForcedWarning("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584), skipping"); + continue; + } + Console.WriteLine("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584)"); + return false; + } + + var split = arg.Value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (split.Length != 2 || !int.TryParse(split[0], out _) || !int.TryParse(split[1], out _)) { // We want to make sure that they *are* ints but the preprocessor takes strings + if(skipBad) { + DMCompiler.ForcedWarning("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584), skipping"); + continue; } + Console.WriteLine("Compiler arg 'version' requires a full BYOND build (e.g. --version=514.1584)"); + return false; + } + settings.DMVersion = split[0]; + settings.DMBuild = split[1]; + break; + } + case null: { // Value-only argument + if (arg.Value is null) // A completely empty argument? This should be a bug. + continue; + if (HasValidDMExtension(arg.Value)) { + settings.Files.Add(arg.Value); break; } - default: { - if (skipBad) { - DMCompiler.ForcedWarning($"Unknown compiler arg '{arg.Name}', skipping"); - break; - } else { - Console.WriteLine($"Unknown arg '{arg}'"); - return false; - } + if (skipBad) { + DMCompiler.ForcedWarning($"Invalid compiler arg '{arg.Value}', skipping"); + } else { + Console.WriteLine($"Invalid arg '{arg}'"); + return false; } + + break; } - } + default: { + if (skipBad) { + DMCompiler.ForcedWarning($"Unknown compiler arg '{arg.Name}', skipping"); + break; + } - if (settings.Files.Count == 0) - { - Console.WriteLine("At least one DME or DM file must be provided as an argument"); - return false; - } else { - foreach(var file in settings.Files) { - Console.WriteLine($"Compiling {Path.GetFileName(file)} on {settings.DMVersion}.{settings.DMBuild}"); + Console.WriteLine($"Unknown arg '{arg}'"); + return false; } } + } - return true; + if (settings.Files.Count == 0) { + PrintHelp(); + return false; } + + foreach(var file in settings.Files) { + Console.WriteLine($"Compiling {Path.GetFileName(file)} on {settings.DMVersion}.{settings.DMBuild}"); + } + + return true; } } From d5b739e22c12b5ef76be6c9294cd95c233c18f6f Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Sat, 9 Dec 2023 22:17:48 +0000 Subject: [PATCH 06/64] Add `BAR` control to DMF support (#1375) * start, don't forget to restore defaultinterface * basic functionality * finish * well that was an easy fix * fix * Some code quality * Use `VerticalExpand` instead of the resize event * Use `Control` instead of `BoxContainer` We're not laying out multiple controls, no need for a container * Replace `[[*]]` in `on-change` --------- Co-authored-by: amy Co-authored-by: wixoaGit --- .../Interface/Controls/ControlBar.cs | 108 ++++++++++++++++++ .../Interface/Controls/ControlWindow.cs | 1 + .../Descriptors/ControlDescriptors.cs | 22 ++++ OpenDreamClient/Interface/DreamStylesheet.cs | 10 ++ 4 files changed, 141 insertions(+) create mode 100644 OpenDreamClient/Interface/Controls/ControlBar.cs diff --git a/OpenDreamClient/Interface/Controls/ControlBar.cs b/OpenDreamClient/Interface/Controls/ControlBar.cs new file mode 100644 index 0000000000..148a7cc2eb --- /dev/null +++ b/OpenDreamClient/Interface/Controls/ControlBar.cs @@ -0,0 +1,108 @@ +using System.Globalization; +using OpenDreamClient.Interface.Descriptors; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; + +namespace OpenDreamClient.Interface.Controls; + +internal sealed class ControlBar : InterfaceControl { + private ProgressBar? _bar; + private Slider? _slider; + private Control _container = default!; // Created by base constructor + + private ControlDescriptorBar BarDescriptor => (ControlDescriptorBar)ElementDescriptor; + + public ControlBar(ControlDescriptor controlDescriptor, ControlWindow window) : base(controlDescriptor, window) { + } + + protected override void UpdateElementDescriptor() { + base.UpdateElementDescriptor(); + + //width + float barWidth = BarDescriptor.Width ?? 10f; + + //TODO dir - these both need RT level changes + //TODO angles + + //is-slider + if (BarDescriptor.IsSlider) { + if (_slider is null) { + _slider = new Slider { + MaxValue = 100, + MinValue = 0, + Margin = new Thickness(4), + HorizontalExpand = true, + VerticalExpand = (barWidth == 0f), + MinHeight = barWidth, + Value = BarDescriptor.Value ?? 0.0f + }; + + _slider.OnValueChanged += OnValueChanged; + + if (_bar is not null) { + _container.RemoveChild(_bar); + _bar = null; + } + + _container.AddChild(_slider); + } else { + _slider.Value = BarDescriptor.Value ?? 0.0f; + } + + //bar-color + if (_slider.TryGetStyleProperty(Slider.StylePropertyGrabber, out var box)) { + box.BackgroundColor = BarDescriptor.BarColor ?? Color.Transparent; + _slider.GrabberStyleBoxOverride = box; + } + } else { + if (_bar is null) { + _bar = new ProgressBar { + MaxValue = 100, + MinValue = 0, + Margin = new Thickness(4), + HorizontalExpand = true, + VerticalExpand = (barWidth == 0f), + MinHeight = barWidth, + Value = BarDescriptor.Value ?? 0.0f + }; + + _bar.OnValueChanged += OnValueChanged; + + if (_slider is not null) { + _container.RemoveChild(_slider); + _slider = null; + } + + _container.AddChild(_bar); + } else { + _bar.Value = BarDescriptor.Value ?? 0.0f; + } + + //bar-color + if (_bar.TryGetStyleProperty(ProgressBar.StylePropertyForeground, out var box)) { + box.BackgroundColor = BarDescriptor.BarColor ?? Color.Transparent; + _bar.ForegroundStyleBoxOverride = box; + } + } + } + + private void OnValueChanged(Robust.Client.UserInterface.Controls.Range range) { + //don't run while you're still sliding, only after + // TODO: RT doesn't update Grabbed until after OnValueChanged, fix that + //if (_slider is not null && _slider.Grabbed) + // return; + + if (BarDescriptor.OnChange != null) { + var valueReplaced = + BarDescriptor.OnChange.Replace("[[*]]", range.Value.ToString(CultureInfo.InvariantCulture)); + + _interfaceManager.RunCommand(valueReplaced); + } + } + + protected override Control CreateUIElement() { + _container = new Control(); + return _container; + } +} diff --git a/OpenDreamClient/Interface/Controls/ControlWindow.cs b/OpenDreamClient/Interface/Controls/ControlWindow.cs index 3fda555b14..bcce13d490 100644 --- a/OpenDreamClient/Interface/Controls/ControlWindow.cs +++ b/OpenDreamClient/Interface/Controls/ControlWindow.cs @@ -218,6 +218,7 @@ public override void AddChild(ElementDescriptor descriptor) { ControlDescriptorLabel => new ControlLabel(controlDescriptor, this), ControlDescriptorGrid => new ControlGrid(controlDescriptor, this), ControlDescriptorTab => new ControlTab(controlDescriptor, this), + ControlDescriptorBar => new ControlBar(controlDescriptor, this), _ => throw new Exception($"Invalid descriptor {controlDescriptor.GetType()}") }; diff --git a/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs b/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs index 5da523cc61..8ab31110cf 100644 --- a/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs +++ b/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs @@ -79,6 +79,7 @@ public WindowDescriptor() { "LABEL" => typeof(ControlDescriptorLabel), "GRID" => typeof(ControlDescriptorGrid), "TAB" => typeof(ControlDescriptorTab), + "BAR" => typeof(ControlDescriptorBar), _ => null }; @@ -163,6 +164,26 @@ public sealed partial class ControlDescriptorTab : ControlDescriptor { } +public sealed partial class ControlDescriptorBar : ControlDescriptor { + [DataField("width")] + public int? Width = 10; //width of the progress bar in pixels. In the default EAST dir, this is more accurately thought of as "height" + [DataField("dir")] + public string? Dir = "east"; //valid values: north/east/south/west/clockwise/cw/counterclockwise/ccw + [DataField("angle1")] + public int? Angle1 = 0; //start angle + [DataField("angle2")] + public int? Angle2 = 180; //end angle + [DataField("bar-color")] + public Color? BarColor = null; //insanely, the default is null which causes the bar not to render regardless of value + [DataField("is-slider")] + public bool IsSlider = false; + [DataField("value")] + public float? Value = 0f; //position of the progress bar + [DataField("on-change")] + public string? OnChange = null; + +} + public sealed class DMFColorSerializer : ITypeReader { public Color Read(ISerializationManager serializationManager, ValueDataNode node, @@ -188,3 +209,4 @@ public ValidationNode Validate(ISerializationManager serializationManager, Value throw new NotImplementedException(); } } + diff --git a/OpenDreamClient/Interface/DreamStylesheet.cs b/OpenDreamClient/Interface/DreamStylesheet.cs index 05480d1cbe..94438a55b5 100644 --- a/OpenDreamClient/Interface/DreamStylesheet.cs +++ b/OpenDreamClient/Interface/DreamStylesheet.cs @@ -171,6 +171,16 @@ public static Stylesheet Make() { }) .Prop("font", notoSansFont10), + //BarControl - composed of ProgressBar and Slider + Element() + .Prop(ProgressBar.StylePropertyBackground, new StyleBoxFlat { BackgroundColor = Color.LightGray, BorderThickness = new Thickness(1), BorderColor = Color.Black}) + .Prop(ProgressBar.StylePropertyForeground, new StyleBoxFlat { BackgroundColor = Color.Transparent, BorderThickness = new Thickness(1), BorderColor = Color.Black}), + Element() + .Prop(Slider.StylePropertyBackground, new StyleBoxFlat { BackgroundColor = Color.Transparent, BorderThickness = new Thickness(1), BorderColor = Color.Black}) + .Prop(Slider.StylePropertyForeground, new StyleBoxFlat { BackgroundColor = Color.LightGray, BorderThickness = new Thickness(1), BorderColor = Color.Black}) + .Prop(Slider.StylePropertyGrabber, new StyleBoxFlat { BackgroundColor = Color.Transparent, BorderThickness = new Thickness(1), BorderColor = Color.Black, ContentMarginLeftOverride=10, ContentMarginRightOverride=10}) + .Prop(Slider.StylePropertyFill, new StyleBoxFlat { BackgroundColor = Color.Transparent, BorderThickness = new Thickness(0), BorderColor = Color.Black}), + }); } } From 063f5e4d532f92977868ad1fa1c8eda92b70dfe6 Mon Sep 17 00:00:00 2001 From: ike709 Date: Mon, 11 Dec 2023 18:04:06 -0700 Subject: [PATCH 07/64] Opcode Metadata (#1515) * Opcode Metadata & Proc Purity * Handle non-static locals * Revert "Handle non-static locals" This reverts commit bbf3398ea38dd752f04e57ebc894ecd41c30d092. * yeet opcode purity * Update DMCompiler/Bytecode/DreamProcOpcode.cs Co-authored-by: wixoa * Update DMCompiler/DM/DMProc.cs Co-authored-by: wixoa * Update DMCompiler/Bytecode/DreamProcOpcode.cs --------- Co-authored-by: ike709 Co-authored-by: rejuvenat0r Co-authored-by: wixoa --- DMCompiler/Bytecode/DreamProcOpcode.cs | 169 ++++++++++++++++--------- DMCompiler/DM/DMProc.cs | 106 ++++------------ 2 files changed, 129 insertions(+), 146 deletions(-) diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index 5c7a1599a8..10bb98d707 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -8,102 +8,102 @@ namespace DMCompiler.Bytecode; public enum DreamProcOpcode : byte { - BitShiftLeft = 0x1, - PushType = 0x2, - PushString = 0x3, + [OpcodeMetadata(stackDelta: -1)] BitShiftLeft = 0x1, + [OpcodeMetadata(stackDelta: 1)] PushType = 0x2, + [OpcodeMetadata(stackDelta: 1)] PushString = 0x3, FormatString = 0x4, - SwitchCaseRange = 0x5, - PushReferenceValue = 0x6, + [OpcodeMetadata(stackDelta: -2)]SwitchCaseRange = 0x5, //This could either shrink the stack by 2 or 3. Assume 2. + [OpcodeMetadata(stackDelta: 1)] PushReferenceValue = 0x6, // TODO: Local refs should be pure, and other refs that aren't modified //0x7 - Add = 0x8, + [OpcodeMetadata(stackDelta: -1)] Add = 0x8, Assign = 0x9, Call = 0xA, MultiplyReference = 0xB, - JumpIfFalse = 0xC, - JumpIfTrue = 0xD, + [OpcodeMetadata(stackDelta: -1)] JumpIfFalse = 0xC, + [OpcodeMetadata(stackDelta: -1)] JumpIfTrue = 0xD, Jump = 0xE, - CompareEquals = 0xF, - Return = 0x10, - PushNull = 0x11, - Subtract = 0x12, - CompareLessThan = 0x13, - CompareGreaterThan = 0x14, - BooleanAnd = 0x15, + [OpcodeMetadata(stackDelta: -1)] CompareEquals = 0xF, + [OpcodeMetadata(stackDelta: -1)] Return = 0x10, + [OpcodeMetadata(stackDelta: 1)] PushNull = 0x11, + [OpcodeMetadata(stackDelta: -1)] Subtract = 0x12, + [OpcodeMetadata(stackDelta: -1)] CompareLessThan = 0x13, + [OpcodeMetadata(stackDelta: -1)] CompareGreaterThan = 0x14, + [OpcodeMetadata(stackDelta: -1)] BooleanAnd = 0x15, //Either shrinks the stack 1 or 0. Assume 1. BooleanNot = 0x16, DivideReference = 0x17, Negate = 0x18, - Modulus = 0x19, + [OpcodeMetadata(stackDelta: -1)] Modulus = 0x19, Append = 0x1A, - CreateRangeEnumerator = 0x1B, + [OpcodeMetadata(stackDelta: -3)] CreateRangeEnumerator = 0x1B, Input = 0x1C, - CompareLessThanOrEqual = 0x1D, + [OpcodeMetadata(stackDelta: -1)] CompareLessThanOrEqual = 0x1D, CreateAssociativeList = 0x1E, Remove = 0x1F, - DeleteObject = 0x20, - PushResource = 0x21, + [OpcodeMetadata(stackDelta: -1)] DeleteObject = 0x20, + [OpcodeMetadata(stackDelta: 1)] PushResource = 0x21, CreateList = 0x22, CallStatement = 0x23, - BitAnd = 0x24, - CompareNotEquals = 0x25, - PushProc = 0x26, - Divide = 0x27, - Multiply = 0x28, + [OpcodeMetadata(stackDelta: -1)] BitAnd = 0x24, + [OpcodeMetadata(stackDelta: -1)] CompareNotEquals = 0x25, + [OpcodeMetadata(stackDelta: 1)] PushProc = 0x26, + [OpcodeMetadata(stackDelta: -1)] Divide = 0x27, + [OpcodeMetadata(stackDelta: -1)] Multiply = 0x28, BitXorReference = 0x29, - BitXor = 0x2A, - BitOr = 0x2B, + [OpcodeMetadata(stackDelta: -1)] BitXor = 0x2A, + [OpcodeMetadata(stackDelta: -1)] BitOr = 0x2B, BitNot = 0x2C, Combine = 0x2D, CreateObject = 0x2E, - BooleanOr = 0x2F, + [OpcodeMetadata(stackDelta: -1)] BooleanOr = 0x2F, // Shrinks the stack by 1 or 0. Assume 1. //0x30 - CompareGreaterThanOrEqual = 0x31, - SwitchCase = 0x32, + [OpcodeMetadata(stackDelta: -1)] CompareGreaterThanOrEqual = 0x31, + [OpcodeMetadata(stackDelta: -1)] SwitchCase = 0x32, //This could either shrink the stack by 1 or 2. Assume 1. Mask = 0x33, //0x34 Error = 0x35, - IsInList = 0x36, + [OpcodeMetadata(stackDelta: -1)] IsInList = 0x36, //0x37 - PushFloat = 0x38, + [OpcodeMetadata(stackDelta: 1)] PushFloat = 0x38, ModulusReference = 0x39, - CreateListEnumerator = 0x3A, + [OpcodeMetadata(stackDelta: -1)] CreateListEnumerator = 0x3A, Enumerate = 0x3B, DestroyEnumerator = 0x3C, - Browse = 0x3D, - BrowseResource = 0x3E, - OutputControl = 0x3F, - BitShiftRight = 0x40, - CreateFilteredListEnumerator = 0x41, - Power = 0x42, + [OpcodeMetadata(stackDelta: -3)] Browse = 0x3D, + [OpcodeMetadata(stackDelta: -3)] BrowseResource = 0x3E, + [OpcodeMetadata(stackDelta: -3)] OutputControl = 0x3F, + [OpcodeMetadata(stackDelta: -1)] BitShiftRight = 0x40, + [OpcodeMetadata(stackDelta: -1)] CreateFilteredListEnumerator = 0x41, + [OpcodeMetadata(stackDelta: -1)] Power = 0x42, //0x43, //0x44 - Prompt = 0x45, - Ftp = 0x46, - Initial = 0x47, + [OpcodeMetadata(stackDelta: -3)] Prompt = 0x45, + [OpcodeMetadata(stackDelta: -3)] Ftp = 0x46, + [OpcodeMetadata(stackDelta: -1)] Initial = 0x47, //0x48 - IsType = 0x49, - LocateCoord = 0x4A, - Locate = 0x4B, + [OpcodeMetadata(stackDelta: -1)] IsType = 0x49, + [OpcodeMetadata(stackDelta: -2)] LocateCoord = 0x4A, + [OpcodeMetadata(stackDelta: -1)] Locate = 0x4B, IsNull = 0x4C, - Spawn = 0x4D, - OutputReference = 0x4E, - Output = 0x4F, + [OpcodeMetadata(stackDelta: -1)] Spawn = 0x4D, + [OpcodeMetadata(stackDelta: -1)] OutputReference = 0x4E, + [OpcodeMetadata(stackDelta: -2)] Output = 0x4F, JumpIfNullDereference = 0x50, - Pop = 0x51, + [OpcodeMetadata(stackDelta: -1)] Pop = 0x51, Prob = 0x52, - IsSaved = 0x53, + [OpcodeMetadata(stackDelta: -1)] IsSaved = 0x53, PickUnweighted = 0x54, PickWeighted = 0x55, - Increment = 0x56, - Decrement = 0x57, - CompareEquivalent = 0x58, - CompareNotEquivalent = 0x59, + [OpcodeMetadata(stackDelta: 1)] Increment = 0x56, + [OpcodeMetadata(stackDelta: 1)] Decrement = 0x57, + [OpcodeMetadata(stackDelta: -1)] CompareEquivalent = 0x58, + [OpcodeMetadata(stackDelta: -1)] CompareNotEquivalent = 0x59, Throw = 0x5A, - IsInRange = 0x5B, + [OpcodeMetadata(stackDelta: -2)] IsInRange = 0x5B, MassConcatenation = 0x5C, - CreateTypeEnumerator = 0x5D, + [OpcodeMetadata(stackDelta: -1)] CreateTypeEnumerator = 0x5D, //0x5E - PushGlobalVars = 0x5F, - ModulusModulus = 0x60, + [OpcodeMetadata(stackDelta: 1)] PushGlobalVars = 0x5F, + [OpcodeMetadata(stackDelta: -1)] ModulusModulus = 0x60, ModulusModulusReference = 0x61, //0x62 //0x63 @@ -112,7 +112,7 @@ public enum DreamProcOpcode : byte { JumpIfTrueReference = 0x66, JumpIfFalseReference = 0x67, DereferenceField = 0x68, - DereferenceIndex = 0x69, + [OpcodeMetadata(stackDelta: -1)] DereferenceIndex = 0x69, DereferenceCall = 0x6A, PopReference = 0x6B, //0x6C @@ -124,9 +124,9 @@ public enum DreamProcOpcode : byte { EnumerateNoAssign = 0x72, Gradient = 0x73, AssignInto = 0x74, - GetStep = 0x75, + [OpcodeMetadata(stackDelta: -1)] GetStep = 0x75, Length = 0x76, - GetDir = 0x77, + [OpcodeMetadata(stackDelta: -1)] GetDir = 0x77, DebuggerBreakpoint = 0x78, Sin = 0x79, Cos = 0x7A, @@ -134,9 +134,9 @@ public enum DreamProcOpcode : byte { ArcSin = 0x7C, ArcCos = 0x7D, ArcTan = 0x7E, - ArcTan2 = 0x7F, + [OpcodeMetadata(stackDelta: -1)] ArcTan2 = 0x7F, Sqrt = 0x80, - Log = 0x81, + [OpcodeMetadata(stackDelta: -1)] Log = 0x81, LogE = 0x82, Abs = 0x83, } @@ -402,3 +402,46 @@ public static string GetOpcodesHash() { return BitConverter.ToString(hashBytes).Replace("-", ""); } } + +/// +/// Custom attribute for declaring metadata for individual opcodes +/// +[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +internal sealed class OpcodeMetadataAttribute : Attribute { + public OpcodeMetadata Metadata; + + public OpcodeMetadataAttribute(int stackDelta = 0) { + Metadata = new OpcodeMetadata(stackDelta); + } +} + +/// +/// Miscellaneous metadata associated with individual opcodes using the attribute +/// +public struct OpcodeMetadata { + public readonly int StackDelta; // Net change in stack size caused by this opcode + + public OpcodeMetadata(int stackDelta = 0) { + StackDelta = stackDelta; + } +} + +/// +/// Automatically builds a cache of the attribute for each opcode +/// +public static class OpcodeMetadataCache { + private static readonly OpcodeMetadata[] MetadataCache = new OpcodeMetadata[256]; + + static OpcodeMetadataCache() { + foreach (DreamProcOpcode opcode in Enum.GetValues(typeof(DreamProcOpcode))) { + var field = typeof(DreamProcOpcode).GetField(opcode.ToString()); + var attribute = Attribute.GetCustomAttribute(field!, typeof(OpcodeMetadataAttribute)); + var metadataAttribute = (OpcodeMetadataAttribute)attribute; + MetadataCache[(byte)opcode] = metadataAttribute?.Metadata ?? new OpcodeMetadata(); + } + } + + public static OpcodeMetadata GetMetadata(DreamProcOpcode opcode) { + return MetadataCache[(byte)opcode]; + } +} diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 4f7320cb1e..71ef94c9ef 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -378,13 +378,11 @@ public void DebugSource(Location location) { } public void PushReferenceValue(DMReference reference) { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushReferenceValue); WriteReference(reference); } public void CreateListEnumerator() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CreateListEnumerator); } @@ -393,18 +391,15 @@ public void CreateFilteredListEnumerator(DreamPath filterType) { DMCompiler.ForcedError($"Cannot filter enumeration by type {filterType}"); } - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CreateFilteredListEnumerator); WriteInt(filterTypeId); } public void CreateTypeEnumerator() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CreateTypeEnumerator); } public void CreateRangeEnumerator() { - ShrinkStack(3); WriteOpcode(DreamProcOpcode.CreateRangeEnumerator); } @@ -432,13 +427,13 @@ public void DestroyEnumerator() { } public void CreateList(int size) { - ShrinkStack(size - 1); //Shrinks by the size of the list, grows by 1 + ResizeStack(-(size - 1)); //Shrinks by the size of the list, grows by 1 WriteOpcode(DreamProcOpcode.CreateList); WriteInt(size); } public void CreateAssociativeList(int size) { - ShrinkStack(size * 2 - 1); //Shrinks by twice the size of the list, grows by 1 + ResizeStack(-(size * 2 - 1)); //Shrinks by twice the size of the list, grows by 1 WriteOpcode(DreamProcOpcode.CreateAssociativeList); WriteInt(size); } @@ -488,45 +483,37 @@ public void LoopEnd() { } public void SwitchCase(string caseLabel) { - ShrinkStack(1); //This could either shrink the stack by 1 or 2. Assume 1. WriteOpcode(DreamProcOpcode.SwitchCase); WriteLabel(caseLabel); } public void SwitchCaseRange(string caseLabel) { - ShrinkStack(2); //This could either shrink the stack by 2 or 3. Assume 2. WriteOpcode(DreamProcOpcode.SwitchCaseRange); WriteLabel(caseLabel); } public void Browse() { - ShrinkStack(3); WriteOpcode(DreamProcOpcode.Browse); } public void BrowseResource() { - ShrinkStack(3); WriteOpcode(DreamProcOpcode.BrowseResource); } public void OutputControl() { - ShrinkStack(3); WriteOpcode(DreamProcOpcode.OutputControl); } public void Ftp() { - ShrinkStack(3); WriteOpcode(DreamProcOpcode.Ftp); } public void OutputReference(DMReference leftRef) { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.OutputReference); WriteReference(leftRef); } public void Output() { - ShrinkStack(2); WriteOpcode(DreamProcOpcode.Output); } @@ -537,7 +524,6 @@ public void Input(DMReference leftRef, DMReference rightRef) { } public void Spawn(string jumpTo) { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Spawn); WriteLabel(jumpTo); } @@ -613,7 +599,6 @@ public void Goto(DMASTIdentifier label) { } public void Pop() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Pop); } @@ -623,13 +608,11 @@ public void PopReference(DMReference reference) { } public void BooleanOr(string endLabel) { - ShrinkStack(1); //Either shrinks the stack 1 or 0. Assume 1. WriteOpcode(DreamProcOpcode.BooleanOr); WriteLabel(endLabel); } public void BooleanAnd(string endLabel) { - ShrinkStack(1); //Either shrinks the stack 1 or 0. Assume 1. WriteOpcode(DreamProcOpcode.BooleanAnd); WriteLabel(endLabel); } @@ -649,13 +632,11 @@ public void Jump(string label) { } public void JumpIfFalse(string label) { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.JumpIfFalse); WriteLabel(label); } public void JumpIfTrue(string label) { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.JumpIfTrue); WriteLabel(label); } @@ -697,12 +678,11 @@ public void DereferenceField(string field) { } public void DereferenceIndex() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.DereferenceIndex); } public void DereferenceCall(string field, DMCallArgumentsType argumentsType, int argumentStackSize) { - ShrinkStack(argumentStackSize); // Pops proc owner and arguments, pushes result + ResizeStack(-argumentStackSize); // Pops proc owner and arguments, pushes result WriteOpcode(DreamProcOpcode.DereferenceCall); WriteString(field); WriteByte((byte)argumentsType); @@ -710,7 +690,7 @@ public void DereferenceCall(string field, DMCallArgumentsType argumentsType, int } public void Call(DMReference reference, DMCallArgumentsType argumentsType, int argumentStackSize) { - ShrinkStack(argumentStackSize - 1); // Pops all arguments, pushes return value + ResizeStack(-(argumentStackSize - 1)); // Pops all arguments, pushes return value WriteOpcode(DreamProcOpcode.Call); WriteReference(reference); WriteByte((byte)argumentsType); @@ -719,25 +699,22 @@ public void Call(DMReference reference, DMCallArgumentsType argumentsType, int a public void CallStatement(DMCallArgumentsType argumentsType, int argumentStackSize) { //Shrinks the stack by argumentStackSize. Could also shrink it by argumentStackSize+1, but assume not. - ShrinkStack(argumentStackSize); + ResizeStack(-argumentStackSize); WriteOpcode(DreamProcOpcode.CallStatement); WriteByte((byte)argumentsType); WriteInt(argumentStackSize); } public void Prompt(DMValueType types) { - ShrinkStack(3); WriteOpcode(DreamProcOpcode.Prompt); WriteInt((int)types); } public void Initial() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Initial); } public void Return() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Return); } @@ -755,14 +732,13 @@ public void AssignInto(DMReference reference) { } public void CreateObject(DMCallArgumentsType argumentsType, int argumentStackSize) { - ShrinkStack(argumentStackSize); // Pops type and arguments, pushes new object + ResizeStack(-argumentStackSize); // Pops type and arguments, pushes new object WriteOpcode(DreamProcOpcode.CreateObject); WriteByte((byte)argumentsType); WriteInt(argumentStackSize); } public void DeleteObject() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.DeleteObject); } @@ -775,17 +751,14 @@ public void Negate() { } public void Add() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Add); } public void Subtract() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Subtract); } public void Multiply() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Multiply); } @@ -795,7 +768,6 @@ public void MultiplyReference(DMReference reference) { } public void Divide() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Divide); } @@ -805,12 +777,10 @@ public void DivideReference(DMReference reference) { } public void Modulus() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Modulus); } public void ModulusModulus() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.ModulusModulus); } @@ -825,7 +795,6 @@ public void ModulusModulusReference(DMReference reference) { } public void Power() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Power); } @@ -835,13 +804,11 @@ public void Append(DMReference reference) { } public void Increment(DMReference reference) { - GrowStack(1); WriteOpcode(DreamProcOpcode.Increment); WriteReference(reference); } public void Decrement(DMReference reference) { - GrowStack(1); WriteOpcode(DreamProcOpcode.Decrement); WriteReference(reference); } @@ -862,7 +829,6 @@ public void Mask(DMReference reference) { } public void BitShiftLeft() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.BitShiftLeft); } @@ -872,7 +838,6 @@ public void BitShiftLeftReference(DMReference reference) { } public void BitShiftRight() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.BitShiftRight); } @@ -886,12 +851,10 @@ public void BinaryNot() { } public void BinaryAnd() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.BitAnd); } public void BinaryXor() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.BitXor); } @@ -901,47 +864,38 @@ public void BinaryXorReference(DMReference reference) { } public void BinaryOr() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.BitOr); } public void Equal() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareEquals); } public void NotEqual() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareNotEquals); } public void Equivalent() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareEquivalent); } public void NotEquivalent() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareNotEquivalent); } public void GreaterThan() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareGreaterThan); } public void GreaterThanOrEqual() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareGreaterThanOrEqual); } public void LessThan() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareLessThan); } public void LessThanOrEqual() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.CompareLessThanOrEqual); } @@ -970,7 +924,6 @@ public void ArcTan() { } public void ArcTan2() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.ArcTan2); } @@ -979,7 +932,6 @@ public void Sqrt() { } public void Log() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Log); } @@ -992,42 +944,35 @@ public void Abs() { } public void PushFloat(float value) { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushFloat); WriteFloat(value); } public void PushString(string value) { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushString); WriteString(value); } public void PushResource(string value) { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushResource); WriteString(value); } public void PushType(int typeId) { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushType); WriteInt(typeId); } public void PushProc(int procId) { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushProc); WriteInt(procId); } public void PushNull() { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushNull); } public void PushGlobalVars() { - GrowStack(1); WriteOpcode(DreamProcOpcode.PushGlobalVars); } @@ -1041,29 +986,25 @@ public void FormatString(string value) { } } - ShrinkStack(formatCount - 1); //Shrinks by the amount of formats in the string, grows 1 + ResizeStack(-(formatCount - 1)); //Shrinks by the amount of formats in the string, grows 1 WriteOpcode(DreamProcOpcode.FormatString); WriteString(value); WriteInt(formatCount); } public void IsInList() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.IsInList); } public void IsInRange() { - ShrinkStack(2); WriteOpcode(DreamProcOpcode.IsInRange); } public void IsSaved() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.IsSaved); } public void IsType() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.IsType); } @@ -1076,35 +1017,32 @@ public void Length() { } public void GetStep() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.GetStep); } public void GetDir() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.GetDir); } public void LocateCoordinates() { - ShrinkStack(2); WriteOpcode(DreamProcOpcode.LocateCoord); } public void Gradient(DMCallArgumentsType argumentsType, int argumentStackSize) { - ShrinkStack(argumentStackSize - 1); // Pops arguments, pushes gradient result + ResizeStack(-(argumentStackSize - 1)); // Pops arguments, pushes gradient result WriteOpcode(DreamProcOpcode.Gradient); WriteByte((byte)argumentsType); WriteInt(argumentStackSize); } public void PickWeighted(int count) { - ShrinkStack(count * 2 - 1); + ResizeStack(-(count * 2 - 1)); WriteOpcode(DreamProcOpcode.PickWeighted); WriteInt(count); } public void PickUnweighted(int count) { - ShrinkStack(count - 1); + ResizeStack(-(count - 1)); WriteOpcode(DreamProcOpcode.PickUnweighted); WriteInt(count); } @@ -1115,13 +1053,12 @@ public void Prob() { } public void MassConcatenation(int count) { - ShrinkStack(count - 1); + ResizeStack(-(count - 1)); WriteOpcode(DreamProcOpcode.MassConcatenation); WriteInt(count); } public void Locate() { - ShrinkStack(1); WriteOpcode(DreamProcOpcode.Locate); } @@ -1142,6 +1079,10 @@ public void EndTry() { private void WriteOpcode(DreamProcOpcode opcode) { _bytecodeWriter.Write((byte)opcode); + + var metadata = OpcodeMetadataCache.GetMetadata(opcode); + + ResizeStack(metadata.StackDelta); } private void WriteByte(byte value) { @@ -1183,7 +1124,7 @@ private void WriteReference(DMReference reference, bool affectStack = true) { case DMReference.Type.Field: WriteString(reference.Name); - ShrinkStack(affectStack ? 1 : 0); + ResizeStack(affectStack ? -1 : 0); break; case DMReference.Type.SrcField: @@ -1192,7 +1133,7 @@ private void WriteReference(DMReference reference, bool affectStack = true) { break; case DMReference.Type.ListIndex: - ShrinkStack(affectStack ? 2 : 0); + ResizeStack(affectStack ? -2 : 0); break; case DMReference.Type.SuperProc: @@ -1207,13 +1148,12 @@ private void WriteReference(DMReference reference, bool affectStack = true) { } } - private void GrowStack(int size) { - _currentStackSize += size; - _maxStackSize = Math.Max(_currentStackSize, _maxStackSize); - } - - private void ShrinkStack(int size) { - _currentStackSize -= size; + /// + /// Tracks the maximum possible stack size of the proc + /// + /// The net change in stack size caused by an operation + private void ResizeStack(int sizeDelta) { + _currentStackSize += sizeDelta; _maxStackSize = Math.Max(_currentStackSize, _maxStackSize); if (_currentStackSize < 0 && !_negativeStackSizeError) { _negativeStackSizeError = true; From 22f7796b00834660525cf3fb58d0fa3f6edd4d97 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Wed, 13 Dec 2023 22:43:31 +0000 Subject: [PATCH 08/64] Add support for tmp savefiles, fix multiple handles for savefiles, clean up (#1535) * add support for tmp savefiles, reduce memory usage, and implement between tick flushing * oops * Fix json fail killing server * add comments + get logger only once * Apply suggestions from code review Co-authored-by: wixoa * copy list before iterating --------- Co-authored-by: amy Co-authored-by: wixoa --- OpenDreamRuntime/DreamManager.cs | 2 +- OpenDreamRuntime/EntryPoint.cs | 4 +- .../Objects/Types/DreamObjectSavefile.cs | 81 +++++++++++++++---- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 3ee77cf2c7..5befb8d19f 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -98,7 +98,7 @@ public void Update() { _procScheduler.Process(); UpdateStat(); _dreamMapManager.UpdateTiles(); - + DreamObjectSavefile.FlushAllUpdates(); WorldInstance.SetVariableValue("cpu", WorldInstance.GetVariable("tick_usage")); } diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 3adbe5fdcd..7897528a8b 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -72,8 +72,8 @@ public override void PostInit() { protected override void Dispose(bool disposing) { // Write every savefile to disk - foreach (var savefile in DreamObjectSavefile.Savefiles) { - savefile.Flush(); + foreach (var savefile in DreamObjectSavefile.Savefiles.ToArray()) { //ToArray() to avoid modifying the collection while iterating over it + savefile.Close(); } _dreamManager.Shutdown(); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs b/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs index 0bb85f7c1d..64f61b92cc 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.IO; +using System.Text.Json; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; @@ -9,15 +10,37 @@ public sealed class DreamObjectSavefile : DreamObject { public sealed class SavefileDirectory : Dictionary { } public static readonly List Savefiles = new(); + //basically a global database of savefile contents, which each savefile datum points to - this preserves state between savefiles and reduces memory usage + private static readonly Dictionary> SavefileDirectories = new(); + private static readonly HashSet _savefilesToFlush = new(); public override bool ShouldCallNew => false; public DreamResource Resource; - public Dictionary Directories; + public Dictionary Directories => SavefileDirectories[Resource.ResourcePath ?? ""]; public SavefileDirectory CurrentDir => Directories[_currentDirPath]; private string _currentDirPath = "/"; + //Temporary savefiles should be deleted when the DreamObjectSavefile is deleted. Temporary savefiles can be created by creating a new savefile datum with a null filename or an entry in the world's resource cache + private bool _isTemporary = false; + + private static ISawmill? _sawmill = null; + /// + /// Flushes all savefiles that have been marked as needing flushing. Basically just used to call Flush() between ticks instead of on every write. + /// + public static void FlushAllUpdates() { + _sawmill ??= Logger.GetSawmill("opendream.res"); + foreach (DreamObjectSavefile savefile in _savefilesToFlush) { + try { + savefile.Flush(); + } catch (Exception e) { + _sawmill.Error($"Error flushing savefile {savefile.Resource.ResourcePath}: {e}"); + } + } + _savefilesToFlush.Clear(); + } + public DreamObjectSavefile(DreamObjectDefinition objectDefinition) : base(objectDefinition) { } @@ -25,26 +48,36 @@ public DreamObjectSavefile(DreamObjectDefinition objectDefinition) : base(object public override void Initialize(DreamProcArguments args) { base.Initialize(args); - string filename = args.GetArgument(0).GetValueAsString(); + args.GetArgument(0).TryGetValueAsString(out string? filename); DreamValue timeout = args.GetArgument(1); //TODO: timeout + if (string.IsNullOrEmpty(filename)) { + _isTemporary = true; + filename = Path.GetTempPath() + "tmp_opendream_savefile_" + System.DateTime.Now.Ticks.ToString(); + } + Resource = DreamResourceManager.LoadResource(filename); - string? data = Resource.ReadAsString(); - if (!string.IsNullOrEmpty(data)) { - Directories = JsonSerializer.Deserialize>(data); - } else { - Directories = new() { - { "/", new SavefileDirectory() } - }; + if(!SavefileDirectories.ContainsKey(filename)) { + //if the savefile hasn't already been loaded, load it or create it + string? data = Resource.ReadAsString(); + + if (!string.IsNullOrEmpty(data)) { + SavefileDirectories.Add(filename, JsonSerializer.Deserialize>(data)); + } else { + SavefileDirectories.Add(filename, new() { + { "/", new SavefileDirectory() } + }); + //create the file immediately + Flush(); + } } Savefiles.Add(this); } protected override void HandleDeletion() { - Savefiles.Remove(this); - + Close(); base.HandleDeletion(); } @@ -53,6 +86,25 @@ public void Flush() { Resource.Output(new DreamValue(JsonSerializer.Serialize(Directories))); } + public void Close() { + Flush(); + if (_isTemporary && Resource.ResourcePath != null) { + File.Delete(Resource.ResourcePath); + } + //check to see if the file is still in use by another savefile datum + if(Resource.ResourcePath != null) { + bool fineToDelete = true; + foreach (var savefile in Savefiles) + if (savefile != this && savefile.Resource.ResourcePath == Resource.ResourcePath) { + fineToDelete = false; + break; + } + if (fineToDelete) + SavefileDirectories.Remove(Resource.ResourcePath); + } + Savefiles.Remove(this); + } + protected override bool TryGetVar(string varName, out DreamValue value) { switch (varName) { case "cd": @@ -102,7 +154,7 @@ public override DreamValue OperatorIndex(DreamValue index) { throw new Exception($"Invalid savefile index {index}"); if (CurrentDir.TryGetValue(entryName, out DreamValue entry)) { - return entry; + return entry; //TODO: This should be something like value.DMProc("Read", new DreamProcArguments(this)) for DreamObjects and a copy for everything else } else { return DreamValue.Null; } @@ -112,7 +164,8 @@ public override void OperatorIndexAssign(DreamValue index, DreamValue value) { if (!index.TryGetValueAsString(out string? entryName)) throw new Exception($"Invalid savefile index {index}"); - CurrentDir[entryName] = value; + CurrentDir[entryName] = value; //TODO: This should be something like value.DMProc("Write", new DreamProcArguments(this)) for DreamObjects and a copy for everything else + _savefilesToFlush.Add(this); //mark this as needing flushing } private void ChangeDirectory(string path) { From 936eac31293b0115af912c5ce5c9c2bdbec3fff1 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 13 Dec 2023 22:35:46 -0500 Subject: [PATCH 09/64] Fix Topic calls dropping partial packets (#1552) --- OpenDreamRuntime/DreamManager.Connections.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 317659734c..5cba48bf6e 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -106,11 +106,18 @@ private async Task ConsumeAndHandleWorldTopicSocket(Socket remote, CancellationT var length = BitConverter.ToUInt16(buffer); buffer = new byte[length]; - var read = await from.ReceiveAsync(buffer, cancellationToken); - if (read != buffer.Length) { - _sawmill.Warning("failed to parse byond topic due to insufficient data read"); - return null; - } + var totalRead = 0; + do { + var read = await from.ReceiveAsync( + new Memory(buffer, totalRead, length - totalRead), + cancellationToken); + if(read == 0 && totalRead != length) { + _sawmill.Warning("failed to parse byond topic due to insufficient data read"); + return null; + } + + totalRead += read; + } while (totalRead < length); return Encoding.ASCII.GetString(buffer[6..^1]); } @@ -178,7 +185,7 @@ private async Task ConsumeAndHandleWorldTopicSocket(Socket remote, CancellationT await remote.DisconnectAsync(false, cancellationToken); } } catch (Exception ex) { - _sawmill.Warning("Error processing topic: {0}", ex); + _sawmill.Warning("Error processing topic #{0}: {1}", topicId, ex); } finally { _sawmill.Debug("Finished world topic #{0}", topicId); } From 4801639618afba0907a56b706bba3108f6c74648 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 14 Dec 2023 21:51:44 -0500 Subject: [PATCH 10/64] Fix TGS test on non-`OpenDream` named repos (#1553) --- .github/workflows/test-tgs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-tgs.yml b/.github/workflows/test-tgs.yml index 92c7c94d4b..e87fc6b13c 100644 --- a/.github/workflows/test-tgs.yml +++ b/.github/workflows/test-tgs.yml @@ -71,7 +71,7 @@ jobs: cd ../tgstation-server/tests/Tgstation.Server.Tests export TGS_TEST_OD_EXCLUSIVE=true export TGS_TEST_OD_ENGINE_VERSION=$GITHUB_SHA - export TGS_TEST_OD_GIT_DIRECTORY="../../../../../../OpenDream" + export TGS_TEST_OD_GIT_DIRECTORY="../../../../../../${{ github.event.repository.name }}" export TGS_TEST_DATABASE_TYPE=Sqlite export TGS_TEST_CONNECTION_STRING="Data Source=TGS.sqlite3;Mode=ReadWriteCreate" dotnet test -c ReleaseNoWindows --no-build --filter Name=TestOpenDreamExclusiveTgsOperation --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --settings ../../build/ci.runsettings --results-directory ../../TestResults From 577e789e91307996e59b510a3cf0142ff4915b92 Mon Sep 17 00:00:00 2001 From: wixoa Date: Sun, 17 Dec 2023 17:22:03 -0500 Subject: [PATCH 11/64] Fix nested escaped brackets (#1555) * Fix nested escaped brackets * Put `DM_ConstantString` in correct alphabetic order * Update DMCompiler/Compiler/DM/DMParserHelper.cs --- .../Tests/Text/NestedEscapedBracket.dm | 4 + DMCompiler/Compiler/DM/DMLexer.cs | 49 +- DMCompiler/Compiler/DM/DMParser.cs | 6 +- DMCompiler/Compiler/DM/DMParserHelper.cs | 527 +++++++++--------- DMCompiler/Compiler/DMM/DMMParser.cs | 8 +- .../Compiler/DMPreprocessor/DMPreprocessor.cs | 5 +- .../DMPreprocessor/DMPreprocessorLexer.cs | 49 +- .../DMPreprocessor/DMPreprocessorParser.cs | 2 +- OpenDreamShared/Compiler/CompilerError.cs | 1 + OpenDreamShared/Compiler/Token.cs | 9 +- 10 files changed, 322 insertions(+), 338 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Text/NestedEscapedBracket.dm diff --git a/Content.Tests/DMProject/Tests/Text/NestedEscapedBracket.dm b/Content.Tests/DMProject/Tests/Text/NestedEscapedBracket.dm new file mode 100644 index 0000000000..07f2ee1edd --- /dev/null +++ b/Content.Tests/DMProject/Tests/Text/NestedEscapedBracket.dm @@ -0,0 +1,4 @@ +// Issue #700 + +/proc/RunTest() + ASSERT("[ "\[" ]" == @"[") \ No newline at end of file diff --git a/DMCompiler/Compiler/DM/DMLexer.cs b/DMCompiler/Compiler/DM/DMLexer.cs index fbd0741f6a..cf2193855a 100644 --- a/DMCompiler/Compiler/DM/DMLexer.cs +++ b/DMCompiler/Compiler/DM/DMLexer.cs @@ -237,7 +237,7 @@ protected override Token ParseNextToken() { string tokenText = preprocToken.Text; switch (preprocToken.Text[0]) { case '"': - case '{': token = CreateToken(TokenType.DM_String, tokenText, preprocToken.Value); break; + case '{': token = CreateToken(TokenType.DM_ConstantString, tokenText, preprocToken.Value); break; case '\'': token = CreateToken(TokenType.DM_Resource, tokenText, preprocToken.Value); break; case '@': token = CreateToken(TokenType.DM_RawString, tokenText, preprocToken.Value); break; default: token = CreateToken(TokenType.Error, tokenText, "Invalid string"); break; @@ -246,45 +246,18 @@ protected override Token ParseNextToken() { Advance(); break; } - case TokenType.DM_Preproc_String: { - string tokenText = preprocToken.Text; - - string? stringStart = null, stringEnd = null; - switch (preprocToken.Text[0]) { - case '"': stringStart = "\""; stringEnd = "\""; break; - case '{': stringStart = "{\""; stringEnd = "\"}"; break; - } - - if (stringStart != null && stringEnd != null) { - TokenTextBuilder.Clear(); - TokenTextBuilder.Append(tokenText); - - int stringNesting = 1; - while (!AtEndOfSource) { - Token stringToken = Advance(); - - TokenTextBuilder.Append(stringToken.Text); - if (stringToken.Type == TokenType.DM_Preproc_String) { - if (stringToken.Text.StartsWith(stringStart)) { - stringNesting++; - } else if (stringToken.Text.EndsWith(stringEnd)) { - stringNesting--; - - if (stringNesting == 0) break; - } - } - } - - string stringText = TokenTextBuilder.ToString(); - string stringValue = stringText.Substring(stringStart.Length, stringText.Length - stringStart.Length - stringEnd.Length); - token = CreateToken(TokenType.DM_String, stringText, stringValue); - } else { - token = CreateToken(TokenType.Error, tokenText, "Invalid string"); - } - + case TokenType.DM_Preproc_StringBegin: + token = CreateToken(TokenType.DM_StringBegin, preprocToken.Text, preprocToken.Value); + Advance(); + break; + case TokenType.DM_Preproc_StringMiddle: + token = CreateToken(TokenType.DM_StringMiddle, preprocToken.Text, preprocToken.Value); + Advance(); + break; + case TokenType.DM_Preproc_StringEnd: + token = CreateToken(TokenType.DM_StringEnd, preprocToken.Text, preprocToken.Value); Advance(); break; - } case TokenType.DM_Preproc_Identifier: { TokenTextBuilder.Clear(); diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 7d667135f4..7aa20b1258 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -2141,6 +2141,7 @@ public void ExpressionTo(out DMASTExpression? endRange, out DMASTExpression? ste primary = ParseProcCall(primary); } } + if (primary == null && Check(TokenType.DM_Call)) { Whitespace(); DMASTCallParameter[]? callParameters = ProcCall(); @@ -2164,7 +2165,10 @@ public void ExpressionTo(out DMASTExpression? endRange, out DMASTExpression? ste case TokenType.DM_Resource: Advance(); return new DMASTConstantResource(constantToken.Location, (string)constantToken.Value); case TokenType.DM_Null: Advance(); return new DMASTConstantNull(constantToken.Location); case TokenType.DM_RawString: Advance(); return new DMASTConstantString(constantToken.Location, (string)constantToken.Value); - case TokenType.DM_String: return ExpressionFromString(constantToken); + case TokenType.DM_ConstantString: + case TokenType.DM_StringBegin: + // Don't advance, ExpressionFromString() will handle it + return ExpressionFromString(); default: return null; } } diff --git a/DMCompiler/Compiler/DM/DMParserHelper.cs b/DMCompiler/Compiler/DM/DMParserHelper.cs index 7ae662d811..03f417875f 100644 --- a/DMCompiler/Compiler/DM/DMParserHelper.cs +++ b/DMCompiler/Compiler/DM/DMParserHelper.cs @@ -95,303 +95,292 @@ private bool TryConvertUtfCodeToString(ReadOnlySpan input, ref StringBuild } /// - /// Handles parsing of Tokens of type .
+ /// Handles parsing of Tokens of type or a series of tokens starting with .
/// (Shunted into a helper because this is a quite long and arduous block of code) ///
/// Either a or a . - private DMASTExpression ExpressionFromString(Token constantToken) { - string tokenValue = (string)constantToken.Value; - StringBuilder stringBuilder = new StringBuilder(tokenValue.Length); // The actual text (but includes special codepoints for macros and markers for where interps go) + private DMASTExpression ExpressionFromString() { + // The actual text (but includes special codepoints for macros and markers for where interps go) + StringBuilder stringBuilder = new(); List? interpolationValues = null; - Advance(); - - int bracketNesting = 0; - StringBuilder? insideBrackets = null; StringFormatEncoder.FormatSuffix currentInterpolationType = StringFormatEncoder.InterpolationDefault; - string usedPrefixMacro = null; // A string holding the name of the last prefix macro (\the, \a etc.) used, for error presentation poipoises + string? usedPrefixMacro = null; // A string holding the name of the last prefix macro (\the, \a etc.) used, for error presentation poipoises bool hasSeenNonRefInterpolation = false; - for (int i = 0; i < tokenValue.Length; i++) { - char c = tokenValue[i]; + var tokenLoc = Current().Location; - if (bracketNesting > 0) { - insideBrackets!.Append(c); // should never be null - } + while (true) { + Token currentToken = Current(); + Advance(); - switch (c) { - case '[': - bracketNesting++; - insideBrackets ??= new StringBuilder(tokenValue.Length - stringBuilder.Length); - interpolationValues ??= new List(1); - break; - case ']' when bracketNesting > 0: { - bracketNesting--; - - if (bracketNesting == 0) { //End of expression - insideBrackets.Remove(insideBrackets.Length - 1, 1); //Remove the ending bracket - - string insideBracketsText = insideBrackets?.ToString(); - if (!String.IsNullOrWhiteSpace(insideBracketsText)) { - DMPreprocessorLexer preprocLexer = new DMPreprocessorLexer(null, constantToken.Location.SourceFile, insideBracketsText); - List preprocTokens = new(); - Token preprocToken; - do { - preprocToken = preprocLexer.NextToken(); - preprocToken.Location = constantToken.Location; - preprocTokens.Add(preprocToken); - } while (preprocToken.Type != TokenType.EndOfFile); - - DMLexer expressionLexer = new DMLexer(constantToken.Location.SourceFile, preprocTokens); - DMParser expressionParser = new DMParser(expressionLexer); - - DMASTExpression? expression = null; - try { - expressionParser.Whitespace(true); - expression = expressionParser.Expression(); - if (expression == null) Error("Expected an expression"); - if (expressionParser.Current().Type != TokenType.EndOfFile) Error("Expected end of embedded statement"); - } catch (CompileErrorException e) { - Emissions.Add(e.Error); - } + string tokenValue = (string)currentToken.Value; - if (expressionParser.Emissions.Count > 0) Emissions.AddRange(expressionParser.Emissions); - interpolationValues.Add(expression); - } else { - interpolationValues.Add(null); - } - hasSeenNonRefInterpolation = hasSeenNonRefInterpolation || currentInterpolationType != StringFormatEncoder.FormatSuffix.ReferenceOfValue; - stringBuilder.Append(StringFormatEncoder.Encode(currentInterpolationType)); + // If an interpolation comes after this, ignore the last character (always '[') + int iterateLength = currentToken.Type is TokenType.DM_StringBegin or TokenType.DM_StringMiddle + ? tokenValue.Length - 1 + : tokenValue.Length; - currentInterpolationType = StringFormatEncoder.InterpolationDefault; - insideBrackets.Clear(); - } + // If an interpolation came before this, ignore the first character (always ']') + int iterateBegin = currentToken.Type is TokenType.DM_StringMiddle or TokenType.DM_StringEnd ? 1 : 0; - break; - } - case '\\' when bracketNesting == 0: { - string escapeSequence = String.Empty; + stringBuilder.EnsureCapacity(stringBuilder.Length + iterateLength - iterateBegin); - if (i == tokenValue.Length) { - Error("Invalid escape sequence"); - } - c = tokenValue[++i]; - - int? utfCodeDigitsExpected = null; - switch (c) { - case 'x': - utfCodeDigitsExpected = 2; break; - case 'u': - utfCodeDigitsExpected = 4; break; - case 'U': - utfCodeDigitsExpected = 6; break; - } + for (int i = iterateBegin; i < iterateLength; i++) { + char c = tokenValue[i]; - if (utfCodeDigitsExpected.HasValue) { - i++; - int utfCodeLength = Math.Min(utfCodeDigitsExpected.Value, tokenValue.Length - i); - var utfCode = tokenValue.AsSpan(i, utfCodeLength); - if (utfCodeLength < utfCodeDigitsExpected.Value || !TryConvertUtfCodeToString(utfCode, ref stringBuilder)) { - Error($"Invalid Unicode macro \"\\{c}{utfCode}\""); - } - i += utfCodeLength - 1; // -1, cause we have i++ in the current 'for' expression - } else if (char.IsLetter(c)) { - while (i < tokenValue.Length && char.IsLetter(tokenValue[i])) { - escapeSequence += tokenValue[i++]; - } - i--; - - bool skipSpaces = false; - bool consumeSpaceCharacter = false; - switch (escapeSequence) { - case "Proper": // Users can have a little case-insensitivity, as a treat - case "Improper": - Warning($"Escape sequence \"\\{escapeSequence}\" should not be capitalized. Coercing macro to \"\\{escapeSequence.ToLower()}"); - escapeSequence = escapeSequence.ToLower(); - goto case "proper"; // Fallthrough! - case "proper": - case "improper": - if (stringBuilder.Length != 0) - { - Error($"Escape sequence \"\\{escapeSequence}\" must come at the beginning of the string"); - } - - skipSpaces = true; - if(escapeSequence == "proper") - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Proper)); - else - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Improper)); - break; - case "roman": - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerRoman)); - break; - case "Roman": - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperRoman)); - break; - - case "ref": - // usedPrefixMacro = true; -- while ref is indeed a prefix macro, it DOES NOT ERROR if it fails to find what it's supposed to /ref. - // TODO: Actually care about this when we add --noparity - currentInterpolationType = StringFormatEncoder.FormatSuffix.ReferenceOfValue; break; - - case "The": - usedPrefixMacro = "The"; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperDefiniteArticle)); - break; - case "the": - usedPrefixMacro = "the"; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerDefiniteArticle)); - break; - - case "A": - case "An": - usedPrefixMacro = escapeSequence; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle)); - break; - case "a": - case "an": - usedPrefixMacro = escapeSequence; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerIndefiniteArticle)); - break; - - case "He": - case "She": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperSubjectPronoun)); - break; - case "he": - case "she": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerSubjectPronoun)); - break; - - case "His": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "His")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessiveAdjective)); - break; - case "his": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "his")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessiveAdjective)); - break; - - case "Him": // BYOND errors here but lets be nice! - Warning("\"\\Him\" is not an available text macro. Coercing macro into \"\\him\""); - goto case "him"; // Fallthrough! - case "him": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "him")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ObjectPronoun)); - break; - - case "Her": - case "her": - Error("\"Her\" is a grammatically ambiguous pronoun. Use \\him or \\his instead"); - break; - - case "himself": - case "herself": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ReflexivePronoun)); - break; - - case "Hers": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "Hers")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessivePronoun)); - break; - case "hers": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "hers")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessivePronoun)); - break; - //Plurals, ordinals, etc - //(things that hug, as a suffix, the [] that they reference) - case "s": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "s")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.PluralSuffix)); - break; - case "th": - if (CheckInterpolation(constantToken.Location, hasSeenNonRefInterpolation, interpolationValues, "th")) break; - // TODO: this should error if not DIRECTLY after an expression ([]\s vs []AA\s) - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.OrdinalIndicator)); - break; - default: - if (escapeSequence.StartsWith("n")) { - stringBuilder.Append('\n'); - stringBuilder.Append(escapeSequence.Skip(1).ToArray()); - } else if (escapeSequence.StartsWith("t")) { - stringBuilder.Append('\t'); - stringBuilder.Append(escapeSequence.Skip(1).ToArray()); - } else if (!DMLexer.ValidEscapeSequences.Contains(escapeSequence)) { // This only exists to allow unimplements to fallthrough w/o a direct error - Error($"Invalid escape sequence \"\\{escapeSequence}\""); - } - - break; + switch (c) { + case '\\': { + string escapeSequence = string.Empty; + + if (i == tokenValue.Length - 1) { + Error("Invalid escape sequence"); } - if (skipSpaces) { - // Note that some macros in BYOND require a single/zero space between them and the [] - // This doesn't replicate that - while (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; + c = tokenValue[++i]; + + int? utfCodeDigitsExpected = null; + switch (c) { + case 'x': + utfCodeDigitsExpected = 2; break; + case 'u': + utfCodeDigitsExpected = 4; break; + case 'U': + utfCodeDigitsExpected = 6; break; } - if(consumeSpaceCharacter) { - if (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; + if (utfCodeDigitsExpected.HasValue) { + i++; + int utfCodeLength = Math.Min(utfCodeDigitsExpected.Value, tokenValue.Length - i); + var utfCode = tokenValue.AsSpan(i, utfCodeLength); + if (utfCodeLength < utfCodeDigitsExpected.Value || !TryConvertUtfCodeToString(utfCode, ref stringBuilder)) { + Error($"Invalid Unicode macro \"\\{c}{utfCode}\""); + } + i += utfCodeLength - 1; // -1, cause we have i++ in the current 'for' expression + } else if (char.IsLetter(c)) { + while (i < tokenValue.Length && char.IsLetter(tokenValue[i])) { + escapeSequence += tokenValue[i++]; + } + i--; + + bool skipSpaces = false; + bool consumeSpaceCharacter = false; + switch (escapeSequence) { + case "Proper": // Users can have a little case-insensitivity, as a treat + case "Improper": + Warning($"Escape sequence \"\\{escapeSequence}\" should not be capitalized. Coercing macro to \"\\{escapeSequence.ToLower()}"); + escapeSequence = escapeSequence.ToLower(); + goto case "proper"; // Fallthrough! + case "proper": + case "improper": + if (stringBuilder.Length != 0) { + Error($"Escape sequence \"\\{escapeSequence}\" must come at the beginning of the string"); + } + + skipSpaces = true; + if(escapeSequence == "proper") + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Proper)); + else + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Improper)); + break; + case "roman": + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerRoman)); + break; + case "Roman": + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperRoman)); + break; + + case "ref": + // usedPrefixMacro = true; -- while ref is indeed a prefix macro, it DOES NOT ERROR if it fails to find what it's supposed to /ref. + // TODO: Actually care about this when we add --noparity + currentInterpolationType = StringFormatEncoder.FormatSuffix.ReferenceOfValue; break; + + case "The": + usedPrefixMacro = "The"; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperDefiniteArticle)); + break; + case "the": + usedPrefixMacro = "the"; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerDefiniteArticle)); + break; + + case "A": + case "An": + usedPrefixMacro = escapeSequence; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle)); + break; + case "a": + case "an": + usedPrefixMacro = escapeSequence; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerIndefiniteArticle)); + break; + + case "He": + case "She": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperSubjectPronoun)); + break; + case "he": + case "she": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerSubjectPronoun)); + break; + + case "His": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "His")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessiveAdjective)); + break; + case "his": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "his")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessiveAdjective)); + break; + + case "Him": // BYOND errors here but lets be nice! + Warning("\"\\Him\" is not an available text macro. Coercing macro into \"\\him\""); + goto case "him"; // Fallthrough! + case "him": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "him")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ObjectPronoun)); + break; + + case "Her": + case "her": + Error("\"Her\" is a grammatically ambiguous pronoun. Use \\him or \\his instead"); + break; + + case "himself": + case "herself": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ReflexivePronoun)); + break; + + case "Hers": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "Hers")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessivePronoun)); + break; + case "hers": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "hers")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessivePronoun)); + break; + //Plurals, ordinals, etc + //(things that hug, as a suffix, the [] that they reference) + case "s": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "s")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.PluralSuffix)); + break; + case "th": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "th")) break; + // TODO: this should error if not DIRECTLY after an expression ([]\s vs []AA\s) + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.OrdinalIndicator)); + break; + default: + if (escapeSequence.StartsWith("n")) { + stringBuilder.Append('\n'); + stringBuilder.Append(escapeSequence.Skip(1).ToArray()); + } else if (escapeSequence.StartsWith("t")) { + stringBuilder.Append('\t'); + stringBuilder.Append(escapeSequence.Skip(1).ToArray()); + } else if (!DMLexer.ValidEscapeSequences.Contains(escapeSequence)) { // This only exists to allow unimplements to fallthrough w/o a direct error + Error($"Invalid escape sequence \"\\{escapeSequence}\""); + } + + break; + } + + if (skipSpaces) { + // Note that some macros in BYOND require a single/zero space between them and the [] + // This doesn't replicate that + while (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; + } + + if(consumeSpaceCharacter) { + if (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; + } } - } - else { - escapeSequence += c; - switch (escapeSequence) { - case "[": - case "]": - case "<": - case ">": - case "\"": - case "'": - case "\\": - case " ": - case ".": - stringBuilder.Append(escapeSequence); - break; - default: //Unimplemented escape sequence - Error("Invalid escape sequence \"\\" + escapeSequence + "\""); - break; + else { + escapeSequence += c; + switch (escapeSequence) { + case "[": + case "]": + case "<": + case ">": + case "\"": + case "'": + case "\\": + case " ": + case ".": + stringBuilder.Append(escapeSequence); + break; + default: //Unimplemented escape sequence + Error("Invalid escape sequence \"\\" + escapeSequence + "\""); + break; + } } - } - break; - } - default: { - if (bracketNesting == 0) { + break; + } + default: { stringBuilder.Append(c); + break; } - - break; } } - } - if (bracketNesting > 0) Error("Expected ']'"); + // We've parsed the text of this piece of string, what happens next depends on what token this was + switch (currentToken.Type) { + case TokenType.DM_ConstantString: // Constant singular piece of string, return here + if (usedPrefixMacro != null) // FIXME: \the should not compiletime here, instead becoming a tab character followed by "he", when in parity mode + DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, tokenLoc, + $"Macro \"\\{usedPrefixMacro}\" requires interpolated expression"); + + return new DMASTConstantString(currentToken.Location, stringBuilder.ToString()); + case TokenType.DM_StringBegin: + case TokenType.DM_StringMiddle: // An interpolation is coming up, collect the expression + interpolationValues ??= new(1); + + Whitespace(); + if (Current().Type is TokenType.DM_StringMiddle or TokenType.DM_StringEnd) { // Empty interpolation + interpolationValues.Add(null); + } else { + var interpolatedExpression = Expression(); + if (interpolatedExpression == null) + DMCompiler.Emit(WarningCode.MissingExpression, Current().Location, + "Expected an embedded expression"); + + // The next token should be the next piece of the string, error if not + if (Current().Type is not TokenType.DM_StringMiddle and not TokenType.DM_StringEnd) { + DMCompiler.Emit(WarningCode.BadExpression, Current().Location, + "Expected end of the embedded expression"); + + while (Current().Type is not TokenType.DM_StringMiddle and not TokenType.DM_StringEnd + and not TokenType.EndOfFile) { + Advance(); + } + } + interpolationValues.Add(interpolatedExpression); + } - string stringValue = stringBuilder.ToString(); - if (interpolationValues is null) { - if (usedPrefixMacro != null) // FIXME: \the should not compiletime here, instead becoming a tab character followed by "he", when in parity mode - DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, constantToken.Location, - $"Macro \"\\{usedPrefixMacro}\" requires interpolated expression"); - return new DMASTConstantString(constantToken.Location, stringValue); - } + hasSeenNonRefInterpolation |= currentInterpolationType != StringFormatEncoder.FormatSuffix.ReferenceOfValue; + stringBuilder.Append(StringFormatEncoder.Encode(currentInterpolationType)); + currentInterpolationType = StringFormatEncoder.InterpolationDefault; + break; + case TokenType.DM_StringEnd: // End of a string with interpolated values, return here + if(currentInterpolationType != StringFormatEncoder.InterpolationDefault) { // this implies a prefix tried to modify a [] that never ended up existing after it + DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, tokenLoc, + $"Macro \"\\{usedPrefixMacro}\" must precede an interpolated expression"); + } - if(currentInterpolationType != StringFormatEncoder.InterpolationDefault) { // this implies a prefix tried to modify a [] that never ended up existing after it - DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, constantToken.Location, - $"Macro \"\\{usedPrefixMacro}\" must precede an interpolated expression"); + return new DMASTStringFormat(tokenLoc, stringBuilder.ToString(), interpolationValues!.ToArray()); + } } - - return new DMASTStringFormat(constantToken.Location, stringValue, interpolationValues.ToArray()); } } } diff --git a/DMCompiler/Compiler/DMM/DMMParser.cs b/DMCompiler/Compiler/DMM/DMMParser.cs index bb236c1c5e..aad08e810f 100644 --- a/DMCompiler/Compiler/DMM/DMMParser.cs +++ b/DMCompiler/Compiler/DMM/DMMParser.cs @@ -52,7 +52,7 @@ public DreamMapJson ParseMap() { public CellDefinitionJson? ParseCellDefinition() { Token currentToken = Current(); - if (Check(TokenType.DM_String)) { + if (Check(TokenType.DM_ConstantString)) { Consume(TokenType.DM_Equals, "Expected '='"); Consume(TokenType.DM_LeftParenthesis, "Expected '('"); @@ -109,9 +109,9 @@ public DreamMapJson ParseMap() { Consume(TokenType.DM_RightParenthesis, "Expected ')'"); return cellDefinition; - } else { - return null; } + + return null; } public MapBlockJson? ParseMapBlock() { @@ -122,7 +122,7 @@ public DreamMapJson ParseMap() { Consume(TokenType.DM_Equals, "Expected '='"); Token blockStringToken = Current(); - Consume(TokenType.DM_String, "Expected a string"); + Consume(TokenType.DM_ConstantString, "Expected a constant string"); string blockString = (string)blockStringToken.Value; List lines = new(blockString.Split("\n", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)); diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs index 36c8541a9c..7dc66d258e 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs @@ -155,7 +155,9 @@ public IEnumerator GetEnumerator() { } case TokenType.DM_Preproc_Punctuator: case TokenType.DM_Preproc_Number: - case TokenType.DM_Preproc_String: + case TokenType.DM_Preproc_StringBegin: + case TokenType.DM_Preproc_StringMiddle: + case TokenType.DM_Preproc_StringEnd: case TokenType.DM_Preproc_ConstantString: case TokenType.DM_Preproc_Punctuator_Comma: case TokenType.DM_Preproc_Punctuator_Period: @@ -169,6 +171,7 @@ public IEnumerator GetEnumerator() { while (_bufferedWhitespace.TryPop(out var whitespace)) { yield return whitespace; } + _currentLineContainsNonWhitespace = true; _canUseDirective = (token.Type == TokenType.DM_Preproc_Punctuator_Semicolon); diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs index e81491b412..24700ab2ec 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs @@ -473,21 +473,29 @@ private bool TryMacroKeyword(string text, [NotNullWhen(true)] out Token? token) /// /// If it contains string interpolations, it splits the string tokens into parts and lex the expressions as normal
/// For example, "There are [amount] of them" becomes:
- /// DM_Preproc_String("There are "), DM_Preproc_Identifier(amount), DM_Preproc_String(" of them")
+ /// DM_Preproc_StringBegin("There are "), DM_Preproc_Identifier(amount), DM_Preproc_StringEnd(" of them")
/// If there is no string interpolation, it outputs a DM_Preproc_ConstantString token instead ///
private Token LexString(bool isLong) { char terminator = GetCurrent(); - StringBuilder textBuilder = new StringBuilder(isLong ? "{" + terminator : char.ToString(terminator)); + StringBuilder textBuilder = new StringBuilder(); Queue stringTokens = new(); + string tokenTextStart = isLong ? "{" + terminator : char.ToString(terminator); + string tokenTextEnd = isLong ? terminator + "}" : char.ToString(terminator); + bool isConstant = true; + bool foundTerminator = false; Advance(); while (!(!isLong && AtLineEnd()) && !AtEndOfSource()) { char stringC = GetCurrent(); - textBuilder.Append(stringC); if (stringC == '[') { - stringTokens.Enqueue(CreateToken(TokenType.DM_Preproc_String, textBuilder.ToString())); + textBuilder.Append(stringC); + stringTokens.Enqueue(isConstant // First case of '[' + ? CreateToken(TokenType.DM_Preproc_StringBegin, tokenTextStart + textBuilder, textBuilder.ToString()) + : CreateToken(TokenType.DM_Preproc_StringMiddle, textBuilder.ToString(), textBuilder.ToString())); + + isConstant = false; textBuilder.Clear(); Advance(); @@ -509,28 +517,27 @@ private Token LexString(bool isLong) { Advance(); if (AtLineEnd()) { //Line splice - //Remove the '\' from textBuilder and ignore newlines & all incoming whitespace - textBuilder.Remove(textBuilder.Length - 1, 1); + // Ignore newlines & all incoming whitespace do { Advance(); } while (AtLineEnd() || GetCurrent() == ' ' || GetCurrent() == '\t'); } else { + textBuilder.Append(stringC); textBuilder.Append(GetCurrent()); Advance(); } } else if (stringC == terminator) { if (isLong) { - stringC = Advance(); - - if (stringC == '}') { - textBuilder.Append('}'); - + if (Advance() == '}') { + foundTerminator = true; break; } } else { + foundTerminator = true; break; } } else { + textBuilder.Append(stringC); Advance(); } } @@ -538,22 +545,20 @@ private Token LexString(bool isLong) { if (!AtEndOfSource() && !HandleLineEnd()) Advance(); - string text = textBuilder.ToString(); - if (!isLong && !(text.EndsWith(terminator) && text.Length != 1)) - return CreateToken(TokenType.Error, string.Empty, "Expected '" + terminator + "' to end string"); - if (isLong && !text.EndsWith("}")) + if (!isLong && !foundTerminator) + return CreateToken(TokenType.Error, string.Empty, $"Expected '{terminator}' to end string"); + if (isLong && !foundTerminator) return CreateToken(TokenType.Error, string.Empty, "Expected '}' to end long string"); - if (stringTokens.Count == 0) { - string stringValue = isLong ? text.Substring(2, text.Length - 4) : text.Substring(1, text.Length - 2); + var text = textBuilder.ToString(); - return CreateToken(TokenType.DM_Preproc_ConstantString, text, stringValue); + if (isConstant) { + return CreateToken(TokenType.DM_Preproc_ConstantString, tokenTextStart + text + tokenTextEnd, text); } else { - stringTokens.Enqueue(CreateToken(TokenType.DM_Preproc_String, textBuilder.ToString())); + foreach (var token in stringTokens) + _pendingTokenQueue.Enqueue(token); - foreach (Token stringToken in stringTokens) { - _pendingTokenQueue.Enqueue(stringToken); - } + _pendingTokenQueue.Enqueue(CreateToken(TokenType.DM_Preproc_StringEnd, text + tokenTextEnd, text)); return _pendingTokenQueue.Dequeue(); } diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs index 34eb175d73..f004e83f20 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs @@ -333,7 +333,7 @@ private static void Error(string msg) { case TokenType.DM_Float: Advance(); return (float)constantToken.Value; - case TokenType.DM_String: { + case TokenType.DM_ConstantString: { Advance(); Error("Strings are not valid in preprocessor expressions. Did you mean to use a define() here?"); return DegenerateValue; diff --git a/OpenDreamShared/Compiler/CompilerError.cs b/OpenDreamShared/Compiler/CompilerError.cs index 0c468ffd54..31a9fa93b7 100644 --- a/OpenDreamShared/Compiler/CompilerError.cs +++ b/OpenDreamShared/Compiler/CompilerError.cs @@ -12,6 +12,7 @@ public enum WarningCode { BadToken = 1, BadDirective = 10, BadExpression = 11, + MissingExpression = 12, BadLabel = 19, InvalidReference = 50, BadArgument = 100, diff --git a/OpenDreamShared/Compiler/Token.cs b/OpenDreamShared/Compiler/Token.cs index 979dfd9840..baa98c2b83 100644 --- a/OpenDreamShared/Compiler/Token.cs +++ b/OpenDreamShared/Compiler/Token.cs @@ -39,7 +39,9 @@ public enum TokenType : byte { DM_Preproc_Punctuator_RightBracket, DM_Preproc_Punctuator_RightParenthesis, DM_Preproc_Punctuator_Semicolon, - DM_Preproc_String, + DM_Preproc_StringBegin, + DM_Preproc_StringMiddle, + DM_Preproc_StringEnd, DM_Preproc_TokenConcat, DM_Preproc_Undefine, DM_Preproc_Warning, @@ -61,6 +63,7 @@ public enum TokenType : byte { DM_Catch, DM_Colon, DM_Comma, + DM_ConstantString, DM_Continue, DM_Dedent, DM_Del, @@ -125,7 +128,9 @@ public enum TokenType : byte { DM_StarEquals, DM_StarStar, DM_Step, - DM_String, + DM_StringBegin, + DM_StringMiddle, + DM_StringEnd, DM_SuperProc, DM_Switch, DM_Throw, From ac0410a246498e8191f274ff01efd5b8c36ff3a9 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 18 Dec 2023 15:22:17 -0500 Subject: [PATCH 12/64] Update `Byond.TopicSender` to v8.0.1 (#1556) Contains fixes for packet splintering, ValueTasks, records/structs and other goodness. --- OpenDreamPackageTool/ServerPackaging.cs | 2 ++ OpenDreamRuntime/OpenDreamRuntime.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/OpenDreamPackageTool/ServerPackaging.cs b/OpenDreamPackageTool/ServerPackaging.cs index 0181c1ea28..ee808c2cff 100644 --- a/OpenDreamPackageTool/ServerPackaging.cs +++ b/OpenDreamPackageTool/ServerPackaging.cs @@ -35,6 +35,7 @@ public static class ServerPackaging { "OpenDreamRuntime", "Byond.TopicSender", "Microsoft.Extensions.Logging.Abstractions", // dep of Byond.TopicSender + "Microsoft.Extensions.DependencyInjection.Abstractions", // dep of above "DMCompiler" }; @@ -45,6 +46,7 @@ public static class ServerPackaging { "OpenDreamRuntime", "Byond.TopicSender", "Microsoft.Extensions.Logging.Abstractions", // dep of Byond.TopicSender + "Microsoft.Extensions.DependencyInjection.Abstractions", // dep of above "DMCompiler" }; diff --git a/OpenDreamRuntime/OpenDreamRuntime.csproj b/OpenDreamRuntime/OpenDreamRuntime.csproj index 0b57032d72..153b919dee 100644 --- a/OpenDreamRuntime/OpenDreamRuntime.csproj +++ b/OpenDreamRuntime/OpenDreamRuntime.csproj @@ -13,7 +13,7 @@ - + From bbadf696a85c14e54f9bc4a33d079c8224e09ff3 Mon Sep 17 00:00:00 2001 From: wixoa Date: Mon, 18 Dec 2023 15:23:54 -0500 Subject: [PATCH 13/64] Remove last uses of `DreamPath` outside of the compiler (#1528) * Remove last uses of `DreamPath` outside of the compiler * Fix error * Fix indentation --- DMCompiler/Compiler/DM/DMASTHelper.cs | 2 +- DMCompiler/Compiler/DM/DMPath.cs | 11 +- DMCompiler/Compiler/DMM/DMMParser.cs | 1 - DMCompiler/DM/DMExpression.cs | 1 - DMCompiler/DM/Expressions/Binary.cs | 1 - DMCompiler/DM/Expressions/Constant.cs | 1 - DMCompiler/DM/Expressions/LValue.cs | 1 - DMCompiler/DreamPath.cs | 245 ++++++++++++++ DMDisassembler/DMType.cs | 35 +- DMDisassembler/Program.cs | 311 +++++++++--------- OpenDreamRuntime/DreamThread.cs | 6 +- OpenDreamRuntime/DreamValue.cs | 6 +- OpenDreamRuntime/Objects/DreamObject.cs | 2 +- .../Objects/DreamObjectDefinition.cs | 2 +- OpenDreamRuntime/Objects/DreamObjectTree.cs | 104 +++--- .../Objects/Types/DreamObjectSavefile.cs | 10 +- OpenDreamRuntime/Procs/AsyncNativeProc.cs | 2 +- OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 2 +- OpenDreamRuntime/Procs/DMProc.cs | 6 +- .../Procs/DebugAdapter/DreamDebugManager.cs | 8 +- .../Procs/Native/DreamProcNative.cs | 2 +- .../Procs/Native/DreamProcNativeRoot.cs | 28 +- OpenDreamRuntime/Procs/NativeProc.cs | 2 +- OpenDreamShared/Dream/DreamPath.cs | 243 -------------- 24 files changed, 514 insertions(+), 518 deletions(-) create mode 100644 DMCompiler/DreamPath.cs delete mode 100644 OpenDreamShared/Dream/DreamPath.cs diff --git a/DMCompiler/Compiler/DM/DMASTHelper.cs b/DMCompiler/Compiler/DM/DMASTHelper.cs index a37890f734..0040d2ef66 100644 --- a/DMCompiler/Compiler/DM/DMASTHelper.cs +++ b/DMCompiler/Compiler/DM/DMASTHelper.cs @@ -72,7 +72,7 @@ public void HashFile(DMASTFile node) { } public DMASTProcDefinition GetProcByPath(string path) { - var h = Hash(new DMASTProcDefinition(Location.Unknown, new OpenDreamShared.Dream.DreamPath(path), new DMASTDefinitionParameter[0], null)); + var h = Hash(new DMASTProcDefinition(Location.Unknown, new DreamPath(path), new DMASTDefinitionParameter[0], null)); return nodes[h][0] as DMASTProcDefinition; } public void HashDefine(DMASTNode node) { diff --git a/DMCompiler/Compiler/DM/DMPath.cs b/DMCompiler/Compiler/DM/DMPath.cs index 6c048ce93f..adc53c7be3 100644 --- a/DMCompiler/Compiler/DM/DMPath.cs +++ b/DMCompiler/Compiler/DM/DMPath.cs @@ -1,12 +1,7 @@ - -using System.Collections.Generic; -using OpenDreamShared.Dream; +using System.Collections.Generic; -namespace DMCompiler.Compiler.DM -{ - - public abstract class VarDeclInfo - { +namespace DMCompiler.Compiler.DM { + public abstract class VarDeclInfo { public DreamPath? TypePath; public string VarName; diff --git a/DMCompiler/Compiler/DMM/DMMParser.cs b/DMCompiler/Compiler/DMM/DMMParser.cs index aad08e810f..e91136570e 100644 --- a/DMCompiler/Compiler/DMM/DMMParser.cs +++ b/DMCompiler/Compiler/DMM/DMMParser.cs @@ -1,6 +1,5 @@ using System; using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; using OpenDreamShared.Json; using DMCompiler.DM; using System.Collections.Generic; diff --git a/DMCompiler/DM/DMExpression.cs b/DMCompiler/DM/DMExpression.cs index eec3d0e949..78cffa0001 100644 --- a/DMCompiler/DM/DMExpression.cs +++ b/DMCompiler/DM/DMExpression.cs @@ -2,7 +2,6 @@ using DMCompiler.DM.Visitors; using OpenDreamShared.Compiler; using DMCompiler.Compiler.DM; -using OpenDreamShared.Dream; using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; diff --git a/DMCompiler/DM/Expressions/Binary.cs b/DMCompiler/DM/Expressions/Binary.cs index 9d1772f401..6a29020177 100644 --- a/DMCompiler/DM/Expressions/Binary.cs +++ b/DMCompiler/DM/Expressions/Binary.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; namespace DMCompiler.DM.Expressions { abstract class BinaryOp : DMExpression { diff --git a/DMCompiler/DM/Expressions/Constant.cs b/DMCompiler/DM/Expressions/Constant.cs index 824f7680c7..aa65c8b80f 100644 --- a/DMCompiler/DM/Expressions/Constant.cs +++ b/DMCompiler/DM/Expressions/Constant.cs @@ -1,5 +1,4 @@ using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; using OpenDreamShared.Json; using System; using System.Collections.Generic; diff --git a/DMCompiler/DM/Expressions/LValue.cs b/DMCompiler/DM/Expressions/LValue.cs index 83c5bf5f42..50bcf8390b 100644 --- a/DMCompiler/DM/Expressions/LValue.cs +++ b/DMCompiler/DM/Expressions/LValue.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; namespace DMCompiler.DM.Expressions { abstract class LValue : DMExpression { diff --git a/DMCompiler/DreamPath.cs b/DMCompiler/DreamPath.cs new file mode 100644 index 0000000000..28a1fe6426 --- /dev/null +++ b/DMCompiler/DreamPath.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace DMCompiler; + +// TODO: This is fairly ugly structure and should probably be removed/redone with something much nicer. +// It's heavily embedded into a lot of corners of the compiler though. +public struct DreamPath { + public static readonly DreamPath Root = new DreamPath("/"); + public static readonly DreamPath Exception = new DreamPath("/exception"); + public static readonly DreamPath List = new DreamPath("/list"); + public static readonly DreamPath Regex = new DreamPath("/regex"); + public static readonly DreamPath Savefile = new DreamPath("/savefile"); + public static readonly DreamPath Sound = new DreamPath("/sound"); + public static readonly DreamPath Image = new DreamPath("/image"); + public static readonly DreamPath Icon = new DreamPath("/icon"); + public static readonly DreamPath MutableAppearance = new DreamPath("/mutable_appearance"); + public static readonly DreamPath World = new DreamPath("/world"); + public static readonly DreamPath Client = new DreamPath("/client"); + public static readonly DreamPath Datum = new DreamPath("/datum"); + public static readonly DreamPath Matrix = new DreamPath("/matrix"); + public static readonly DreamPath Atom = new DreamPath("/atom"); + public static readonly DreamPath Area = new DreamPath("/area"); + public static readonly DreamPath Turf = new DreamPath("/turf"); + public static readonly DreamPath Movable = new DreamPath("/atom/movable"); + public static readonly DreamPath Obj = new DreamPath("/obj"); + public static readonly DreamPath Mob = new DreamPath("/mob"); + public static readonly DreamPath Filter = new DreamPath("/dm_filter"); + + public enum PathType { + Absolute, + Relative, + + //TODO: These really shouldn't be here + DownwardSearch, + UpwardSearch + } + + [JsonIgnore] + public string? LastElement { + get => Elements.Length > 0 ? Elements.Last() : null; + } + + [JsonIgnore] + public string[] Elements { + get => _elements; + set { + _elements = value; + _pathString = null; + } + } + + public string PathString { + get { + if (_pathString != null) return _pathString; + + _pathString = Type switch { + PathType.Absolute => "/", + PathType.DownwardSearch => ":", + PathType.UpwardSearch => ".", + _ => string.Empty + }; + + // Elements is usually small enough for this to be faster than StringBuilder + _pathString += string.Join("/", Elements); + + return _pathString; + } + set => SetFromString(value); + } + + public PathType Type; + + private string[] _elements; + private string? _pathString; + + public DreamPath(string path) { + Type = PathType.Absolute; + _elements = Array.Empty(); // Set in SetFromString() + _pathString = null; + + SetFromString(path); + } + + public DreamPath(PathType type, string[] elements) { + Type = type; + _elements = elements; + _pathString = null; + + Normalize(true); + } + + public void SetFromString(string rawPath) { + char pathTypeChar = rawPath[0]; + string[] tempElements = rawPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + bool skipFirstChar = false; + + switch (pathTypeChar) { + case '/': + Type = PathType.Absolute; + // No need to skip the first char, as it will end up as an empty entry in tempElements + break; + case ':': + Type = PathType.DownwardSearch; + skipFirstChar = true; + break; + case '.': + Type = PathType.UpwardSearch; + skipFirstChar = true; + break; + default: + Type = PathType.Relative; + break; + } + + if (skipFirstChar) { + // Skip the '/', ':' or '.' if needed + tempElements[0] = tempElements[0][1..]; + } + + Elements = tempElements; + Normalize(false); + } + + /// + /// Checks if the DreamPath is a descendant of another. NOTE: For type inheritance, use IsSubtypeOf() + /// + /// Path to compare to. + public bool IsDescendantOf(DreamPath path) { + if (path.Elements.Length > Elements.Length) return false; + + for (int i = 0; i < path.Elements.Length; i++) { + if (Elements[i] != path.Elements[i]) return false; + } + + return true; + } + + public DreamPath AddToPath(string path) { + string rawPath = PathString; + + if (!rawPath.EndsWith('/') && !path.StartsWith('/')) { + path = '/' + path; + } + + return new DreamPath(rawPath + path); + } + + public int FindElement(string element) { + return Array.IndexOf(Elements, element); + } + + public string[] GetElements(int elementStart, int elementEnd = -1) { + if (elementEnd < 0) elementEnd = Elements.Length + elementEnd + 1; + + string[] elements = new string[elementEnd - elementStart]; + Array.Copy(Elements, elementStart, elements, 0, elements.Length); + + return elements; + } + + public DreamPath FromElements(int elementStart, int elementEnd = -1) { + string[] elements = GetElements(elementStart, elementEnd); + string rawPath = String.Empty; + + if (elements.Length >= 1) { + rawPath = elements.Aggregate((string first, string second) => { + return first + "/" + second; + }); + } + + rawPath = "/" + rawPath; + return new DreamPath(rawPath); + } + + public DreamPath RemoveElement(int elementIndex) { + if (elementIndex < 0) elementIndex += Elements.Length; + + List elements = new List(); + elements.AddRange(GetElements(0, elementIndex)); + elements.AddRange(GetElements(Math.Min(elementIndex + 1, Elements.Length), -1)); + return new DreamPath(Type, elements.ToArray()); + } + + public DreamPath Combine(DreamPath path) { + switch (path.Type) { + case PathType.Relative: return new DreamPath(PathString + "/" + path.PathString); + case PathType.Absolute: return path; + default: return new DreamPath(PathString + path.PathString); + } + } + + public override string ToString() { + return PathString; + } + + public override bool Equals(object? obj) => obj is DreamPath other && Equals(other); + + public bool Equals(DreamPath other) { + if (other.Elements.Length != Elements.Length) return false; + + for (int i = 0; i < Elements.Length; i++) { + if (Elements[i] != other.Elements[i]) return false; + } + + return true; + } + + public override int GetHashCode() { + int hashCode = 0; + for (int i = 0; i < Elements.Length; i++) { + hashCode += Elements[i].GetHashCode(); + } + + return hashCode; + } + + public static bool operator ==(DreamPath lhs, DreamPath rhs) => lhs.Equals(rhs); + + public static bool operator !=(DreamPath lhs, DreamPath rhs) => !(lhs == rhs); + + private void Normalize(bool canHaveEmptyEntries) { + if (canHaveEmptyEntries && _elements.Contains("")) { + // Slow path :( + _elements = _elements.Where(el => !string.IsNullOrEmpty(el)).ToArray(); + } + + var writeIdx = Array.IndexOf(_elements, ".."); + if (writeIdx == -1) return; + + for (var i = writeIdx; i < _elements.Length; i++) { + var elem = _elements[i]; + if (elem == "..") { + writeIdx -= 1; + } else { + _elements[writeIdx] = elem; + writeIdx += 1; + } + } + + Elements = _elements[..writeIdx]; + } +} diff --git a/DMDisassembler/DMType.cs b/DMDisassembler/DMType.cs index d88695b709..6d200a4412 100644 --- a/DMDisassembler/DMType.cs +++ b/DMDisassembler/DMType.cs @@ -1,27 +1,26 @@ -using OpenDreamShared.Dream; -using OpenDreamShared.Json; +using OpenDreamShared.Json; using System.Collections.Generic; -namespace DMDisassembler { - class DMType { - public DreamPath Path; - public DreamTypeJson Json; - public DMProc InitProc; - public Dictionary Procs; +namespace DMDisassembler; - public DMType(DreamTypeJson json) { - Json = json; - Path = new DreamPath(Json.Path); +internal class DMType { + public string Path; + public DreamTypeJson Json; + public DMProc InitProc; + public Dictionary Procs; - InitProc = Json.InitProc.HasValue ? Program.Procs[Json.InitProc.Value] : null; + public DMType(DreamTypeJson json) { + Json = json; + Path = Json.Path; - Procs = new(json.Procs?.Count ?? 0); - if (Json.Procs != null) { - foreach (List procIds in Json.Procs) { - DMProc proc = Program.Procs[procIds[^1]]; + InitProc = Json.InitProc.HasValue ? Program.Procs[Json.InitProc.Value] : null; - Procs.Add(proc.Name, proc); - } + Procs = new(json.Procs?.Count ?? 0); + if (Json.Procs != null) { + foreach (List procIds in Json.Procs) { + DMProc proc = Program.Procs[procIds[^1]]; + + Procs.Add(proc.Name, proc); } } } diff --git a/DMDisassembler/Program.cs b/DMDisassembler/Program.cs index 16ee005a75..92b266eed8 100644 --- a/DMDisassembler/Program.cs +++ b/DMDisassembler/Program.cs @@ -2,214 +2,213 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; -using OpenDreamShared.Dream; using OpenDreamShared.Json; -namespace DMDisassembler { - class Program { - public static DreamCompiledJson CompiledJson; - public static DMProc GlobalInitProc = null; - public static List Procs = null; - public static Dictionary AllTypes = null; +namespace DMDisassembler; - private static readonly string NoTypeSelectedMessage = "No type is selected"; +internal class Program { + public static DreamCompiledJson CompiledJson; + public static DMProc GlobalInitProc = null; + public static List Procs = null; + public static Dictionary AllTypes = null; - private static DMType _selectedType = null; + private static readonly string NoTypeSelectedMessage = "No type is selected"; - static void Main(string[] args) { - if (args.Length == 0 || Path.GetExtension(args[0]) != ".json") { - Console.WriteLine("The json output of DMCompiler must be provided as an argument"); + private static DMType _selectedType = null; - return; - } + static void Main(string[] args) { + if (args.Length == 0 || Path.GetExtension(args[0]) != ".json") { + Console.WriteLine("The json output of DMCompiler must be provided as an argument"); - string compiledJsonText = File.ReadAllText(args[0]); + return; + } - CompiledJson = JsonSerializer.Deserialize(compiledJsonText); - if (CompiledJson.GlobalInitProc != null) GlobalInitProc = new DMProc(CompiledJson.GlobalInitProc); - LoadAllProcs(); - LoadAllTypes(); + string compiledJsonText = File.ReadAllText(args[0]); - bool acceptingCommands = true; - while (acceptingCommands) { - if (_selectedType != null) { - Console.Write(_selectedType.Path); - } - Console.Write("> "); + CompiledJson = JsonSerializer.Deserialize(compiledJsonText); + if (CompiledJson.GlobalInitProc != null) GlobalInitProc = new DMProc(CompiledJson.GlobalInitProc); + LoadAllProcs(); + LoadAllTypes(); - string input = Console.ReadLine(); - if (input == null) { - // EOF - break; - } - string[] split = input.Split(" "); - string command = split[0].ToLower(); - - switch (command) { - case "q": acceptingCommands = false; break; - case "search": Search(split); break; - case "sel": - case "select": Select(split); break; - case "list": List(split); break; - case "d": - case "decompile": Decompile(split); break; - case "test-all": TestAll(); break; - default: Console.WriteLine("Invalid command \"" + command + "\""); break; - } + bool acceptingCommands = true; + while (acceptingCommands) { + if (_selectedType != null) { + Console.Write(_selectedType.Path); } - } - - private static void Search(string[] args) { - if (args.Length < 3) { - Console.WriteLine("search type|proc [name]"); + Console.Write("> "); - return; + string input = Console.ReadLine(); + if (input == null) { + // EOF + break; } + string[] split = input.Split(" "); + string command = split[0].ToLower(); - string type = args[1]; - string name = args[2]; - if (type == "type") { - foreach (DreamPath typePath in AllTypes.Keys) { - if (typePath.PathString.Contains(name)) Console.WriteLine(typePath); - } - } else if (type == "proc") { - if (_selectedType == null) { - Console.WriteLine(NoTypeSelectedMessage); + switch (command) { + case "q": acceptingCommands = false; break; + case "search": Search(split); break; + case "sel": + case "select": Select(split); break; + case "list": List(split); break; + case "d": + case "decompile": Decompile(split); break; + case "test-all": TestAll(); break; + default: Console.WriteLine("Invalid command \"" + command + "\""); break; + } + } + } - return; - } + private static void Search(string[] args) { + if (args.Length < 3) { + Console.WriteLine("search type|proc [name]"); - foreach (string procName in _selectedType.Procs.Keys) { - if (procName.Contains(name)) Console.WriteLine(procName); - } - } else { - Console.WriteLine("Invalid search type \"" + type + "\""); - } + return; } - private static void Select(string[] args) { - if (args.Length < 2) { - Console.WriteLine("select [type]"); + string type = args[1]; + string name = args[2]; + if (type == "type") { + foreach (string typePath in AllTypes.Keys) { + if (typePath.Contains(name)) Console.WriteLine(typePath); + } + } else if (type == "proc") { + if (_selectedType == null) { + Console.WriteLine(NoTypeSelectedMessage); return; } - string type = args[1]; - if (AllTypes.TryGetValue(new DreamPath(type), out DMType dmType)) { - _selectedType = dmType; - } else { - Console.WriteLine("Invalid type \"" + type + "\""); + foreach (string procName in _selectedType.Procs.Keys) { + if (procName.Contains(name)) Console.WriteLine(procName); } + } else { + Console.WriteLine("Invalid search type \"" + type + "\""); } + } - private static void List(string[] args) { - if (args.Length < 2) { - Console.WriteLine("list procs|globals"); + private static void Select(string[] args) { + if (args.Length < 2) { + Console.WriteLine("select [type]"); - return; - } + return; + } - string what = args[1]; - switch (what) { - case "procs": - if (_selectedType == null) { - Console.WriteLine(NoTypeSelectedMessage); - break; - } + string type = args[1]; + if (AllTypes.TryGetValue(type, out DMType dmType)) { + _selectedType = dmType; + } else { + Console.WriteLine("Invalid type \"" + type + "\""); + } + } - foreach (string procName in _selectedType.Procs.Keys) { - Console.WriteLine(procName); - } + private static void List(string[] args) { + if (args.Length < 2) { + Console.WriteLine("list procs|globals"); - break; - case "globals": - if (CompiledJson.Globals == null) { - Console.WriteLine("There are no globals"); - break; - } - - for (int i = 0; i < CompiledJson.Globals.GlobalCount; i++) { - Console.Write(i); - Console.Write(": "); - Console.WriteLine(CompiledJson.Globals.Names[i]); - } + return; + } + string what = args[1]; + switch (what) { + case "procs": + if (_selectedType == null) { + Console.WriteLine(NoTypeSelectedMessage); break; - } - } + } - private static void Decompile(string[] args) { - if (args.Length < 2) { - Console.WriteLine("decompile [name]"); + foreach (string procName in _selectedType.Procs.Keys) { + Console.WriteLine(procName); + } - return; - } + break; + case "globals": + if (CompiledJson.Globals == null) { + Console.WriteLine("There are no globals"); + break; + } - string name = args[1]; - if (name == "" || (name == "" && (_selectedType == null || _selectedType.Path == DreamPath.Root))) { - if (GlobalInitProc != null) { - Console.WriteLine(GlobalInitProc.Decompile()); - } else { - Console.WriteLine("There is no global init proc"); + for (int i = 0; i < CompiledJson.Globals.GlobalCount; i++) { + Console.Write(i); + Console.Write(": "); + Console.WriteLine(CompiledJson.Globals.Names[i]); } - return; - } + break; + } + } - if (_selectedType == null) { - Console.WriteLine(NoTypeSelectedMessage); - return; - } + private static void Decompile(string[] args) { + if (args.Length < 2) { + Console.WriteLine("decompile [name]"); - if (name == "") { - if (_selectedType.InitProc != null) { - Console.WriteLine(_selectedType.InitProc.Decompile()); - } else { - Console.WriteLine("Selected type does not have an init proc"); - } - } else if (_selectedType.Procs.TryGetValue(name, out DMProc proc)) { - Console.WriteLine(proc.Decompile()); + return; + } + + string name = args[1]; + if (name == "" || (name == "" && (_selectedType == null || _selectedType.Path == "/"))) { + if (GlobalInitProc != null) { + Console.WriteLine(GlobalInitProc.Decompile()); } else { - Console.WriteLine("No procs named \"" + name + "\""); + Console.WriteLine("There is no global init proc"); } + + return; } - private static void LoadAllProcs() { - Procs = new List(CompiledJson.Procs.Length); + if (_selectedType == null) { + Console.WriteLine(NoTypeSelectedMessage); + return; + } - foreach (ProcDefinitionJson procDef in CompiledJson.Procs) { - Procs.Add(new DMProc(procDef)); + if (name == "") { + if (_selectedType.InitProc != null) { + Console.WriteLine(_selectedType.InitProc.Decompile()); + } else { + Console.WriteLine("Selected type does not have an init proc"); } + } else if (_selectedType.Procs.TryGetValue(name, out DMProc proc)) { + Console.WriteLine(proc.Decompile()); + } else { + Console.WriteLine("No procs named \"" + name + "\""); } + } - private static void LoadAllTypes() { - AllTypes = new Dictionary(CompiledJson.Types.Length); + private static void LoadAllProcs() { + Procs = new List(CompiledJson.Procs.Length); - foreach (DreamTypeJson json in CompiledJson.Types) { - AllTypes.Add(new DreamPath(json.Path), new DMType(json)); - } + foreach (ProcDefinitionJson procDef in CompiledJson.Procs) { + Procs.Add(new DMProc(procDef)); + } + } - //Add global procs to the root type - DMType globalType = AllTypes[DreamPath.Root]; - foreach (int procId in CompiledJson.GlobalProcs) { - var proc = Procs[procId]; + private static void LoadAllTypes() { + AllTypes = new Dictionary(CompiledJson.Types.Length); - globalType.Procs.Add(proc.Name, proc); - } + foreach (DreamTypeJson json in CompiledJson.Types) { + AllTypes.Add(json.Path, new DMType(json)); } - private static void TestAll() { - int errored = 0, all = 0; - foreach (DMProc proc in Procs) { - string value = proc.Decompile(); - if (proc.exception != null) { - Console.WriteLine("Error disassembling " + proc.Name); - Console.WriteLine(value); - ++errored; - } - ++all; + //Add global procs to the root type + DMType globalType = AllTypes["/"]; + foreach (int procId in CompiledJson.GlobalProcs) { + var proc = Procs[procId]; + + globalType.Procs.Add(proc.Name, proc); + } + } + + private static void TestAll() { + int errored = 0, all = 0; + foreach (DMProc proc in Procs) { + string value = proc.Decompile(); + if (proc.exception != null) { + Console.WriteLine("Error disassembling " + proc.Name); + Console.WriteLine(value); + ++errored; } - Console.WriteLine($"Errors in {errored}/{all} procs"); + ++all; } + Console.WriteLine($"Errors in {errored}/{all} procs"); } } diff --git a/OpenDreamRuntime/DreamThread.cs b/OpenDreamRuntime/DreamThread.cs index f796d038d2..50f7307c8f 100644 --- a/OpenDreamRuntime/DreamThread.cs +++ b/OpenDreamRuntime/DreamThread.cs @@ -18,7 +18,7 @@ public enum ProcStatus { public abstract class DreamProc { public readonly int Id; - public readonly DreamPath OwningType; + public readonly TreeEntry OwningType; public readonly string Name; public readonly bool IsVerb; @@ -38,7 +38,7 @@ public abstract class DreamProc { private readonly string? _verbName; private readonly string? _verbDesc; - protected DreamProc(int id, DreamPath 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, string? verbName, string? verbCategory, string? verbDesc, sbyte invisibility, bool isVerb = false) { Id = id; OwningType = owningType; Name = name; @@ -88,7 +88,7 @@ public DreamValue GetField(string field) { public override string ToString() { var procElement = (SuperProc == null) ? (IsVerb ? "verb/" : "proc/") : String.Empty; // Has "proc/" only if it's not an override - return OwningType == DreamPath.Root ? $"/{procElement}{Name}" : $"{OwningType}/{procElement}{Name}"; + return $"{OwningType.Path}{(OwningType.Path.EndsWith('/') ? string.Empty : "/")}{procElement}{Name}"; } } diff --git a/OpenDreamRuntime/DreamValue.cs b/OpenDreamRuntime/DreamValue.cs index e179338265..43ca2e9b03 100644 --- a/OpenDreamRuntime/DreamValue.cs +++ b/OpenDreamRuntime/DreamValue.cs @@ -380,7 +380,7 @@ public string Stringify() { return rscPath.ResourcePath; case DreamValueType.DreamType: TryGetValueAsType(out var type); - return type.Path.PathString; + return type.Path; case DreamValueType.DreamProc: var proc = MustGetValueAsProc(); @@ -460,7 +460,7 @@ public override void Write(Utf8JsonWriter writer, DreamValue value, JsonSerializ if (dreamObject == null) { writer.WriteNull("Value"); } else { - writer.WriteString("Value", dreamObject.ObjectDefinition.Type.PathString); + writer.WriteString("Value", dreamObject.ObjectDefinition.Type); if (dreamObject is not DreamObjectIcon icon) { throw new NotImplementedException($"Json serialization for {value} is not implemented"); @@ -502,7 +502,7 @@ public override DreamValue Read(ref Utf8JsonReader reader, Type typeToConvert, J if (objectTypePath == null) { value = DreamValue.Null; } else { - var objectDef = _objectTree.GetTreeEntry(new DreamPath(objectTypePath)).ObjectDefinition; + var objectDef = _objectTree.GetTreeEntry(objectTypePath).ObjectDefinition; if (!objectDef.IsSubtypeOf(_objectTree.Icon)) { throw new NotImplementedException($"Json deserialization for type {objectTypePath} is not implemented"); } diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs index 303559e56d..0b41ba1d01 100644 --- a/OpenDreamRuntime/Objects/DreamObject.cs +++ b/OpenDreamRuntime/Objects/DreamObject.cs @@ -427,7 +427,7 @@ public override string ToString() { return $"{ObjectDefinition.Type}{{name=\"{name}\"}}"; } - return ObjectDefinition.Type.ToString(); + return ObjectDefinition.Type; } } } diff --git a/OpenDreamRuntime/Objects/DreamObjectDefinition.cs b/OpenDreamRuntime/Objects/DreamObjectDefinition.cs index c6eab43de4..123fd002d6 100644 --- a/OpenDreamRuntime/Objects/DreamObjectDefinition.cs +++ b/OpenDreamRuntime/Objects/DreamObjectDefinition.cs @@ -31,7 +31,7 @@ public sealed class DreamObjectDefinition { public readonly MetaDataSystem? MetaDataSystem; public readonly TreeEntry TreeEntry; - public DreamPath Type => TreeEntry.Path; + public string Type => TreeEntry.Path; public DreamObjectDefinition? Parent => TreeEntry.ParentEntry?.ObjectDefinition; public int? InitializationProc; public bool NoConstructors { diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index fc3b4d7c3a..22f19fbccd 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -46,7 +46,7 @@ public sealed class DreamObjectTree { public TreeEntry Obj { get; private set; } public TreeEntry Mob { get; private set; } - private readonly Dictionary _pathToType = new(); + private readonly Dictionary _pathToType = new(); private Dictionary _globalProcIds; [Dependency] private readonly AtomManager _atomManager = default!; @@ -67,6 +67,12 @@ public sealed class DreamObjectTree { private MetaDataSystem? _metaDataSystem; public void LoadJson(DreamCompiledJson json) { + var types = json.Types ?? Array.Empty(); + if (types.Length == 0 || types[0].Path != "/") + throw new ArgumentException("The first type must be root!", nameof(json)); + + Root = new("/", 0); + _entitySystemManager.TryGetEntitySystem(out _appearanceSystem); _entitySystemManager.TryGetEntitySystem(out _transformSystem); _entitySystemManager.TryGetEntitySystem(out _pvsOverrideSystem); @@ -74,21 +80,18 @@ public void LoadJson(DreamCompiledJson json) { Strings = json.Strings ?? new(); if (json.GlobalInitProc is { } initProcDef) { - GlobalInitProc = new DMProc(0, DreamPath.Root, initProcDef, "", _dreamManager, _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler); + GlobalInitProc = new DMProc(0, Root, initProcDef, "", _dreamManager, _atomManager, _dreamMapManager, _dreamDebugManager, _dreamResourceManager, this, _procScheduler); } else { GlobalInitProc = null; } - var types = json.Types ?? Array.Empty(); var procs = json.Procs; var globalProcs = json.GlobalProcs; - // Load procs first so types can set their init proc's super proc - LoadProcsFromJson(types, procs, globalProcs); - LoadTypesFromJson(types); + LoadTypesFromJson(types, procs, globalProcs); } - public TreeEntry GetTreeEntry(DreamPath path) { + public TreeEntry GetTreeEntry(string path) { if (!_pathToType.TryGetValue(path, out TreeEntry? type)) { throw new Exception($"Object '{path}' does not exist"); } @@ -100,7 +103,7 @@ public TreeEntry GetTreeEntry(int typeId) { return Types[typeId]; } - public bool TryGetTreeEntry(DreamPath path, [NotNullWhen(true)] out TreeEntry? treeEntry) { + public bool TryGetTreeEntry(string path, [NotNullWhen(true)] out TreeEntry? treeEntry) { return _pathToType.TryGetValue(path, out treeEntry); } @@ -247,40 +250,41 @@ public DreamValue GetDreamValueFromJsonElement(object? value) { } } - private void LoadTypesFromJson(DreamTypeJson[] types) { - Dictionary pathToTypeId = new(); + private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? procs, int[]? globalProcs) { Types = new TreeEntry[types.Length]; //First pass: Create types and set them up for initialization - for (int i = 0; i < Types.Length; i++) { - DreamPath path = new DreamPath(types[i].Path); + Types[0] = Root; + for (int i = 1; i < Types.Length; i++) { + var path = types[i].Path; var type = new TreeEntry(path, i); Types[i] = type; _pathToType[path] = type; - pathToTypeId[path] = i; } - Root = GetTreeEntry(DreamPath.Root); - World = GetTreeEntry(DreamPath.World); - List = GetTreeEntry(DreamPath.List); - Client = GetTreeEntry(DreamPath.Client); - Datum = GetTreeEntry(DreamPath.Datum); - Sound = GetTreeEntry(DreamPath.Sound); - Matrix = GetTreeEntry(DreamPath.Matrix); - Exception = GetTreeEntry(DreamPath.Exception); - Savefile = GetTreeEntry(DreamPath.Savefile); - Regex = GetTreeEntry(DreamPath.Regex); - Filter = GetTreeEntry(DreamPath.Filter); - Icon = GetTreeEntry(DreamPath.Icon); - Image = GetTreeEntry(DreamPath.Image); - MutableAppearance = GetTreeEntry(DreamPath.MutableAppearance); - Atom = GetTreeEntry(DreamPath.Atom); - Area = GetTreeEntry(DreamPath.Area); - Turf = GetTreeEntry(DreamPath.Turf); - Movable = GetTreeEntry(DreamPath.Movable); - Obj = GetTreeEntry(DreamPath.Obj); - Mob = GetTreeEntry(DreamPath.Mob); + World = GetTreeEntry("/world"); + List = GetTreeEntry("/list"); + Client = GetTreeEntry("/client"); + Datum = GetTreeEntry("/datum"); + Sound = GetTreeEntry("/sound"); + Matrix = GetTreeEntry("/matrix"); + Exception = GetTreeEntry("/exception"); + Savefile = GetTreeEntry("/savefile"); + Regex = GetTreeEntry("/regex"); + Filter = GetTreeEntry("/dm_filter"); + Icon = GetTreeEntry("/icon"); + Image = GetTreeEntry("/image"); + MutableAppearance = GetTreeEntry("/mutable_appearance"); + Atom = GetTreeEntry("/atom"); + Area = GetTreeEntry("/area"); + Turf = GetTreeEntry("/turf"); + Movable = GetTreeEntry("/atom/movable"); + Obj = GetTreeEntry("/obj"); + Mob = GetTreeEntry("/mob"); + + // Load procs first so types can set their init proc's super proc + LoadProcsFromJson(procs, globalProcs); //Second pass: Set each type's parent and children for (int i = 0; i < Types.Length; i++) { @@ -300,7 +304,7 @@ private void LoadTypesFromJson(DreamTypeJson[] types) { //Thus, the enumeration of GetAllDescendants() uint treeIndex = 0; foreach (TreeEntry type in GetAllDescendants(Root)) { - int typeId = pathToTypeId[type.Path]; + int typeId = type.Id; DreamTypeJson jsonType = types[typeId]; var definition = new DreamObjectDefinition(_dreamManager, this, _atomManager, _dreamMapManager, _mapManager, _dreamResourceManager, _walkManager, _entityManager, _playerManager, _serializationManager, _appearanceSystem, _transformSystem, _pvsOverrideSystem, _metaDataSystem, type); @@ -341,7 +345,7 @@ private void LoadTypesFromJson(DreamTypeJson[] types) { //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.Path.LastElement!.Replace("_", " ")); + type.ObjectDefinition.Variables["name"] = new(type.Name.Replace("_", " ")); if (type.ObjectDefinition.Variables["text"].IsNull && type.ObjectDefinition.Variables["name"].TryGetValueAsString(out var name)) { type.ObjectDefinition.Variables["text"] = new DreamValue(string.IsNullOrEmpty(name) ? string.Empty : name[..1]); @@ -379,19 +383,19 @@ private void LoadVariablesFromJson(DreamObjectDefinition objectDefinition, Dream } } - public DreamProc LoadProcJson(int id, DreamTypeJson[] types, ProcDefinitionJson procDefinition) { - DreamPath owningType = new DreamPath(types[procDefinition.OwningTypeId].Path); + 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); } - private void LoadProcsFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? jsonProcs, int[]? jsonGlobalProcs) { + private void LoadProcsFromJson(ProcDefinitionJson[]? jsonProcs, int[]? jsonGlobalProcs) { Procs.Clear(); if (jsonProcs != null) { Procs.EnsureCapacity(jsonProcs.Length); foreach (var proc in jsonProcs) { - Procs.Add(LoadProcJson(Procs.Count, types, proc)); + Procs.Add(LoadProcJson(Procs.Count, proc)); } } @@ -406,7 +410,7 @@ private void LoadProcsFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? json } } - internal NativeProc CreateNativeProc(DreamPath owningType, NativeProc.HandlerFn func) { + internal NativeProc CreateNativeProc(TreeEntry owningType, NativeProc.HandlerFn func) { var (name, defaultArgumentValues, argumentNames) = NativeProc.GetNativeInfo(func); var proc = new NativeProc(Procs.Count, owningType, name, argumentNames, defaultArgumentValues, func, _dreamManager, _atomManager, _dreamMapManager, _dreamResourceManager, _walkManager, this); @@ -414,7 +418,7 @@ internal NativeProc CreateNativeProc(DreamPath owningType, NativeProc.HandlerFn return proc; } - private AsyncNativeProc CreateAsyncNativeProc(DreamPath owningType, Func> func) { + private AsyncNativeProc CreateAsyncNativeProc(TreeEntry owningType, Func> func) { var (name, defaultArgumentValues, argumentNames) = NativeProc.GetNativeInfo(func); var proc = new AsyncNativeProc(Procs.Count, owningType, name, argumentNames, defaultArgumentValues, func); @@ -424,26 +428,26 @@ private AsyncNativeProc CreateAsyncNativeProc(DreamPath owningType, Func> func) { var (name, defaultArgumentValues, argumentNames) = NativeProc.GetNativeInfo(func); - var proc = new AsyncNativeProc(_globalProcIds[name], DreamPath.Root, name, argumentNames, defaultArgumentValues, func); + var proc = new AsyncNativeProc(_globalProcIds[name], Root, name, argumentNames, defaultArgumentValues, func); Procs[proc.Id] = proc; } internal void SetNativeProc(TreeEntry type, NativeProc.HandlerFn func) { - var proc = CreateNativeProc(type.Path, func); + var proc = CreateNativeProc(type, func); type.ObjectDefinition.SetProcDefinition(proc.Name, proc.Id); } public void SetNativeProc(TreeEntry type, Func> func) { - var proc = CreateAsyncNativeProc(type.Path, func); + var proc = CreateAsyncNativeProc(type, func); type.ObjectDefinition.SetProcDefinition(proc.Name, proc.Id); } @@ -464,7 +468,8 @@ private IEnumerable TraversePostOrder(TreeEntry from) { } public sealed class TreeEntry { - public DreamPath Path; + public readonly string Name; + public readonly string Path; public readonly int Id; public DreamObjectDefinition ObjectDefinition; public TreeEntry ParentEntry; @@ -481,7 +486,10 @@ public sealed class TreeEntry { /// public uint ChildCount; - public TreeEntry(DreamPath path, int id) { + public TreeEntry(string path, int id) { + int lastSlash = path.LastIndexOf('/'); + + Name = (lastSlash != -1) ? path.Substring(lastSlash + 1) : path; Path = path; Id = id; } @@ -493,6 +501,6 @@ public bool IsSubtypeOf(TreeEntry ancestor) { } public override string ToString() { - return Path.PathString; + return Path; } } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs b/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs index 64f61b92cc..1900d780a0 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectSavefile.cs @@ -169,10 +169,12 @@ public override void OperatorIndexAssign(DreamValue index, DreamValue value) { } private void ChangeDirectory(string path) { - _currentDirPath = new DreamPath(_currentDirPath).AddToPath(path).PathString; - - if (!Directories.ContainsKey(_currentDirPath)) { - Directories.Add(_currentDirPath, new SavefileDirectory()); + if (path.StartsWith('/')) { + _currentDirPath = path; + } else { + _currentDirPath += path; } + + Directories.TryAdd(_currentDirPath, new SavefileDirectory()); } } diff --git a/OpenDreamRuntime/Procs/AsyncNativeProc.cs b/OpenDreamRuntime/Procs/AsyncNativeProc.cs index cc9576358f..ff86a77f2d 100644 --- a/OpenDreamRuntime/Procs/AsyncNativeProc.cs +++ b/OpenDreamRuntime/Procs/AsyncNativeProc.cs @@ -169,7 +169,7 @@ public DreamValue GetArgument(int argumentPosition, string argumentName) { private readonly Dictionary? _defaultArgumentValues; private readonly Func> _taskFunc; - public AsyncNativeProc(int id, DreamPath owningType, string name, List argumentNames, Dictionary defaultArgumentValues, 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) { _defaultArgumentValues = defaultArgumentValues; _taskFunc = taskFunc; diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index d3d0ff4675..40a5f7049f 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -167,7 +167,7 @@ public static ProcStatus CreateObject(DMProcState state) { var val = state.Pop(); if (!val.TryGetValueAsType(out var objectType)) { if (val.TryGetValueAsString(out var pathString)) { - if (!state.Proc.ObjectTree.TryGetTreeEntry(new DreamPath(pathString), out objectType)) { + if (!state.Proc.ObjectTree.TryGetTreeEntry(pathString, out objectType)) { ThrowCannotCreateUnknownObject(val); } } else { diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index b8c155ed82..a5d2a3c796 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -29,7 +29,7 @@ public sealed class DMProc : DreamProc { private readonly int _maxStackSize; - public DMProc(int id, DreamPath owningType, ProcDefinitionJson json, string? name, DreamManager dreamManager, AtomManager atomManager, IDreamMapManager dreamMapManager, IDreamDebugManager dreamDebugManager, DreamResourceManager dreamResourceManager, DreamObjectTree objectTree, ProcScheduler procScheduler) + 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) { Bytecode = json.Bytecode ?? Array.Empty(); LocalNames = json.Locals; @@ -425,8 +425,8 @@ public override void ReturnedInto(DreamValue value) { } public override void AppendStackFrame(StringBuilder builder) { - if (Proc.OwningType != DreamPath.Root) { - builder.Append(Proc.OwningType.ToString()); + if (Proc.OwningType != Proc.ObjectTree.Root) { + builder.Append(Proc.OwningType); builder.Append('/'); } diff --git a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs index be15bd06db..46d5b59cf5 100644 --- a/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs +++ b/OpenDreamRuntime/Procs/DebugAdapter/DreamDebugManager.cs @@ -150,7 +150,7 @@ public void HandleFirstResume(DMProcState state) { // Check for a function breakpoint List? hit = null; - if (_possibleFunctionBreakpoints.TryGetValue((state.Proc.OwningType.PathString, state.Proc.Name), out var slot)) { + if (_possibleFunctionBreakpoints.TryGetValue((state.Proc.OwningType.Path, state.Proc.Name), out var slot)) { foreach (var bp in slot.Breakpoints) { if (TestBreakpoint(bp)) { hit ??= new(1); @@ -160,7 +160,7 @@ public void HandleFirstResume(DMProcState state) { } if (hit != null) { - Output($"Function breakpoint hit at {state.Proc.OwningType.PathString}::{state.Proc.Name}"); + Output($"Function breakpoint hit at {state.Proc.OwningType.Path}::{state.Proc.Name}"); Stop(state.Thread, new StoppedEvent { Reason = StoppedEvent.ReasonFunctionBreakpoint, HitBreakpointIds = hit @@ -422,7 +422,7 @@ private void InitializePossibleBreakpoints() { private IEnumerable<(string Type, string Proc)> IterateProcs() { foreach (var proc in _objectTree.Procs) { - yield return (proc.OwningType.PathString, proc.Name); + yield return (proc.OwningType.Path, proc.Name); } } @@ -692,7 +692,7 @@ private void HandleRequestScopes(DebugAdapterClient client, RequestScopes reques } private IEnumerable ExpandArguments(RequestVariables req, DMProcState dmFrame) { - if (dmFrame.Proc.OwningType != OpenDreamShared.Dream.DreamPath.Root) { + if (dmFrame.Proc.OwningType != _objectTree.Root) { yield return DescribeValue("src", new(dmFrame.Instance)); } yield return DescribeValue("usr", new(dmFrame.Usr)); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index c078826a2e..d4dabd739c 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -155,7 +155,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { /// Sets a native proc that can be overriden by DM code /// private static void SetOverridableNativeProc(DreamObjectTree objectTree, TreeEntry type, NativeProc.HandlerFn func) { - var nativeProc = objectTree.CreateNativeProc(type.Path, func); + var nativeProc = objectTree.CreateNativeProc(type, func); var proc = objectTree.World.ObjectDefinition.GetProc(nativeProc.Name); if (proc.SuperProc == null) { // This proc was never overriden so just replace it diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 4489bcd9a6..224e5b5ea2 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -1110,7 +1110,7 @@ private static void JsonEncode(Utf8JsonWriter writer, DreamValue value) { } else if (value.TryGetValueAsString(out var text)) writer.WriteStringValue(text); else if (value.TryGetValueAsType(out var type)) - writer.WriteStringValue(type.Path.PathString); + writer.WriteStringValue(type.Path); else if (value.TryGetValueAsProc(out var proc)) writer.WriteStringValue(proc.ToString()); else if (value.TryGetValueAsDreamList(out var list)) { @@ -2508,17 +2508,15 @@ public static DreamValue NativeProc_text2num(NativeProc.Bundle bundle, DreamObje [DreamProc("text2path")] [DreamProcParameter("T", Type = DreamValueTypeFlag.String)] public static DreamValue NativeProc_text2path(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { - if (!bundle.GetArgument(0, "T").TryGetValueAsString(out var text) || string.IsNullOrWhiteSpace(text)) { + if (!bundle.GetArgument(0, "T").TryGetValueAsString(out var path) || string.IsNullOrWhiteSpace(path)) { return DreamValue.Null; } - DreamPath path = new DreamPath(text); - bool isVerb = false; - int procElementIndex = path.FindElement("proc"); + int procElementIndex = path.IndexOf("/proc/", StringComparison.Ordinal); if (procElementIndex == -1) { - procElementIndex = path.FindElement("verb"); + procElementIndex = path.IndexOf("/verb/", StringComparison.Ordinal); if (procElementIndex != -1) isVerb = true; } @@ -2527,17 +2525,17 @@ public static DreamValue NativeProc_text2path(NativeProc.Bundle bundle, DreamObj string? procName = null; if (isProcPath) { - procName = path.LastElement; + procName = path.Substring(path.LastIndexOf('/') + 1); if (procElementIndex == 0) { // global procs - if (procName != null && bundle.ObjectTree.TryGetGlobalProc(procName, out var globalProc) && globalProc.IsVerb == isVerb) + if (bundle.ObjectTree.TryGetGlobalProc(procName, out var globalProc) && globalProc.IsVerb == isVerb) return new DreamValue(globalProc); else return DreamValue.Null; } } - DreamPath typePath = isProcPath ? path.FromElements(0, procElementIndex) : path; + string typePath = isProcPath ? path.Substring(0, procElementIndex) : path; if (!bundle.ObjectTree.TryGetTreeEntry(typePath, out var type) || type == bundle.ObjectTree.Root) return DreamValue.Null; @@ -2724,16 +2722,14 @@ public static DreamValue NativeProc_typesof(NativeProc.Bundle bundle, DreamObjec type = typeObj.ObjectDefinition.TreeEntry; } else if (typeValue.TryGetValueAsString(out var typeString)) { - DreamPath path = new DreamPath(typeString); - - if (path.LastElement == "proc") { - type = bundle.ObjectTree.GetTreeEntry(path.FromElements(0, -2)); + if (typeString.EndsWith("/proc")) { + type = bundle.ObjectTree.GetTreeEntry(typeString.Substring(0, typeString.Length - 5)); addingProcs = type.ObjectDefinition.Procs.Values; - } else if (path.LastElement == "verb") { - type = bundle.ObjectTree.GetTreeEntry(path.FromElements(0, -2)); + } else if (typeString.EndsWith("/verb")) { + type = bundle.ObjectTree.GetTreeEntry(typeString.Substring(0, typeString.Length - 5)); addingProcs = type.ObjectDefinition.Verbs; } else { - type = bundle.ObjectTree.GetTreeEntry(path); + type = bundle.ObjectTree.GetTreeEntry(typeString); } } else { continue; diff --git a/OpenDreamRuntime/Procs/NativeProc.cs b/OpenDreamRuntime/Procs/NativeProc.cs index a61de283a6..66384711b0 100644 --- a/OpenDreamRuntime/Procs/NativeProc.cs +++ b/OpenDreamRuntime/Procs/NativeProc.cs @@ -83,7 +83,7 @@ private DreamValue GetArgumentFallback(string argumentName) { private readonly Dictionary? _defaultArgumentValues; private readonly delegate* _handler; - public NativeProc(int id, DreamPath owningType, string name, List argumentNames, Dictionary defaultArgumentValues, HandlerFn handler, DreamManager dreamManager, AtomManager atomManager, IDreamMapManager mapManager, DreamResourceManager resourceManager, WalkManager walkManager, DreamObjectTree objectTree) + 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) { _defaultArgumentValues = defaultArgumentValues; _handler = (delegate*)handler.Method.MethodHandle.GetFunctionPointer(); diff --git a/OpenDreamShared/Dream/DreamPath.cs b/OpenDreamShared/Dream/DreamPath.cs deleted file mode 100644 index 66e8101294..0000000000 --- a/OpenDreamShared/Dream/DreamPath.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; - -namespace OpenDreamShared.Dream { - public struct DreamPath { - public static readonly DreamPath Root = new DreamPath("/"); - public static readonly DreamPath Exception = new DreamPath("/exception"); - public static readonly DreamPath List = new DreamPath("/list"); - public static readonly DreamPath Regex = new DreamPath("/regex"); - public static readonly DreamPath Savefile = new DreamPath("/savefile"); - public static readonly DreamPath Sound = new DreamPath("/sound"); - public static readonly DreamPath Image = new DreamPath("/image"); - public static readonly DreamPath Icon = new DreamPath("/icon"); - public static readonly DreamPath MutableAppearance = new DreamPath("/mutable_appearance"); - public static readonly DreamPath World = new DreamPath("/world"); - public static readonly DreamPath Client = new DreamPath("/client"); - public static readonly DreamPath Datum = new DreamPath("/datum"); - public static readonly DreamPath Matrix = new DreamPath("/matrix"); - public static readonly DreamPath Atom = new DreamPath("/atom"); - public static readonly DreamPath Area = new DreamPath("/area"); - public static readonly DreamPath Turf = new DreamPath("/turf"); - public static readonly DreamPath Movable = new DreamPath("/atom/movable"); - public static readonly DreamPath Obj = new DreamPath("/obj"); - public static readonly DreamPath Mob = new DreamPath("/mob"); - public static readonly DreamPath Filter = new DreamPath("/dm_filter"); - - public enum PathType { - Absolute, - Relative, - - //TODO: These really shouldn't be here - DownwardSearch, - UpwardSearch - } - - [JsonIgnore] - public string? LastElement { - get => Elements.Length > 0 ? Elements.Last() : null; - } - - [JsonIgnore] - public string[] Elements { - get => _elements; - set { - _elements = value; - _pathString = null; - } - } - - public string PathString { - get { - if (_pathString != null) return _pathString; - - _pathString = Type switch { - PathType.Absolute => "/", - PathType.DownwardSearch => ":", - PathType.UpwardSearch => ".", - _ => string.Empty - }; - - // Elements is usually small enough for this to be faster than StringBuilder - _pathString += string.Join("/", Elements); - - return _pathString; - } - set => SetFromString(value); - } - - public PathType Type; - - private string[] _elements; - private string? _pathString; - - public DreamPath(string path) { - Type = PathType.Absolute; - _elements = Array.Empty(); // Set in SetFromString() - _pathString = null; - - SetFromString(path); - } - - public DreamPath(PathType type, string[] elements) { - Type = type; - _elements = elements; - _pathString = null; - - Normalize(true); - } - - public void SetFromString(string rawPath) { - char pathTypeChar = rawPath[0]; - string[] tempElements = rawPath.Split('/', StringSplitOptions.RemoveEmptyEntries); - bool skipFirstChar = false; - - switch (pathTypeChar) { - case '/': - Type = PathType.Absolute; - // No need to skip the first char, as it will end up as an empty entry in tempElements - break; - case ':': - Type = PathType.DownwardSearch; - skipFirstChar = true; - break; - case '.': - Type = PathType.UpwardSearch; - skipFirstChar = true; - break; - default: - Type = PathType.Relative; - break; - } - - if (skipFirstChar) { - // Skip the '/', ':' or '.' if needed - tempElements[0] = tempElements[0][1..]; - } - - Elements = tempElements; - Normalize(false); - } - - /// - /// Checks if the DreamPath is a descendant of another. NOTE: For type inheritance, use IsSubtypeOf() - /// - /// Path to compare to. - public bool IsDescendantOf(DreamPath path) { - if (path.Elements.Length > Elements.Length) return false; - - for (int i = 0; i < path.Elements.Length; i++) { - if (Elements[i] != path.Elements[i]) return false; - } - - return true; - } - - public DreamPath AddToPath(string path) { - string rawPath = PathString; - - if (!rawPath.EndsWith('/') && !path.StartsWith('/')) { - path = '/' + path; - } - - return new DreamPath(rawPath + path); - } - - public int FindElement(string element) { - return Array.IndexOf(Elements, element); - } - - public string[] GetElements(int elementStart, int elementEnd = -1) { - if (elementEnd < 0) elementEnd = Elements.Length + elementEnd + 1; - - string[] elements = new string[elementEnd - elementStart]; - Array.Copy(Elements, elementStart, elements, 0, elements.Length); - - return elements; - } - - public DreamPath FromElements(int elementStart, int elementEnd = -1) { - string[] elements = GetElements(elementStart, elementEnd); - string rawPath = String.Empty; - - if (elements.Length >= 1) { - rawPath = elements.Aggregate((string first, string second) => { - return first + "/" + second; - }); - } - - rawPath = "/" + rawPath; - return new DreamPath(rawPath); - } - - public DreamPath RemoveElement(int elementIndex) { - if (elementIndex < 0) elementIndex += Elements.Length; - - List elements = new List(); - elements.AddRange(GetElements(0, elementIndex)); - elements.AddRange(GetElements(Math.Min(elementIndex + 1, Elements.Length), -1)); - return new DreamPath(Type, elements.ToArray()); - } - - public DreamPath Combine(DreamPath path) { - switch (path.Type) { - case PathType.Relative: return new DreamPath(PathString + "/" + path.PathString); - case PathType.Absolute: return path; - default: return new DreamPath(PathString + path.PathString); - } - } - - public override string ToString() { - return PathString; - } - - public override bool Equals(object? obj) => obj is DreamPath other && Equals(other); - - public bool Equals(DreamPath other) { - if (other.Elements.Length != Elements.Length) return false; - - for (int i = 0; i < Elements.Length; i++) { - if (Elements[i] != other.Elements[i]) return false; - } - - return true; - } - - public override int GetHashCode() { - int hashCode = 0; - for (int i = 0; i < Elements.Length; i++) { - hashCode += Elements[i].GetHashCode(); - } - - return hashCode; - } - - public static bool operator ==(DreamPath lhs, DreamPath rhs) => lhs.Equals(rhs); - - public static bool operator !=(DreamPath lhs, DreamPath rhs) => !(lhs == rhs); - - private void Normalize(bool canHaveEmptyEntries) { - if (canHaveEmptyEntries && _elements.Contains("")) { - // Slow path :( - _elements = _elements.Where(el => !string.IsNullOrEmpty(el)).ToArray(); - } - - var writeIdx = Array.IndexOf(_elements, ".."); - if (writeIdx == -1) return; - - for (var i = writeIdx; i < _elements.Length; i++) { - var elem = _elements[i]; - if (elem == "..") { - writeIdx -= 1; - } else { - _elements[writeIdx] = elem; - writeIdx += 1; - } - } - - Elements = _elements[..writeIdx]; - } - } -} From cf1eb4107fec126017e68730687c38b7d2a207b2 Mon Sep 17 00:00:00 2001 From: wixoa Date: Mon, 18 Dec 2023 16:38:26 -0500 Subject: [PATCH 14/64] Fix `"` appearing in long strings (#1557) * Fix `"` appearing in long strings * Ignore \r in long strings --- Content.Tests/DMProject/Tests/Text/LongString.dm | 6 ++++++ DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 Content.Tests/DMProject/Tests/Text/LongString.dm diff --git a/Content.Tests/DMProject/Tests/Text/LongString.dm b/Content.Tests/DMProject/Tests/Text/LongString.dm new file mode 100644 index 0000000000..c57f01f4e1 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Text/LongString.dm @@ -0,0 +1,6 @@ +/proc/RunTest() + ASSERT({"A +B +C"} == "A\nB\nC") + + ASSERT({" " "} == " \" ") \ No newline at end of file diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs index 24700ab2ec..1b287d6d82 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs @@ -532,12 +532,16 @@ private Token LexString(bool isLong) { foundTerminator = true; break; } + + textBuilder.Append(stringC); } else { foundTerminator = true; break; } } else { - textBuilder.Append(stringC); + if (stringC != '\r') // \r\n becomes \n + textBuilder.Append(stringC); + Advance(); } } From 99db63928b7c9d6693e8c4611ee99acea60282e2 Mon Sep 17 00:00:00 2001 From: Altoids1 Date: Wed, 20 Dec 2023 15:43:04 -0600 Subject: [PATCH 15/64] Improves error emission of switch blocks, adds new pragma (#1131) * Improves error emission in switch blocks, adds OD3201 Turns out, BYOND actually is OK with "else if" appearing in a switch block, but takes it to mean an 'else' of the switch, followed by an if-elseif-else ladder within that case block. Beestation apparently uses this in production, but it's a very odd and ambiguous style (Ike told me he had to fix a bug caused by this recently in Para) so I've made a style warning for it. This patch also includes some auditing of DMParser Error() emissions so that switch case parsing throws exceptions less often (which reduces code skipping). I do not love how git submodules work edit: was missing one comma * Adds an obsolete notice for DMParser calling base.Error() DMParser should be emitting actual error codes and avoiding throwing when possible (so as to minimize code-skipping). * Address reviews, other minor fixes * Fix a merge conflict issue --------- Co-authored-by: wixoaGit --- .../Statements/Switch/weird_when_clause.dm | 15 +++++++ .../Switch/weird_when_clause_pragma.dm | 16 +++++++ DMCompiler/Compiler/DM/DMParser.cs | 42 ++++++++++--------- DMCompiler/Compiler/DM/DMParserHelper.cs | 10 +++-- DMCompiler/DMStandard/DefaultPragmaConfig.dm | 1 + OpenDreamShared/Compiler/CompilerError.cs | 3 +- 6 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause.dm create mode 100644 Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause_pragma.dm diff --git a/Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause.dm b/Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause.dm new file mode 100644 index 0000000000..777fa60c11 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause.dm @@ -0,0 +1,15 @@ + +//Issue OD#996, kinda: https://github.com/OpenDreamProject/OpenDream/issues/996 + +/proc/RunTest() + var/x = 5 + switch(x) + if(1) + CRASH("Strange branch chosen in switch statement") + if(4) + CRASH("Strange branch chosen in switch statement") + else if(x == 3) + CRASH("Parser failed to understand 'else if' in switch block") + else + return + CRASH("Parser failed to understand 'else if' in switch block") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause_pragma.dm b/Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause_pragma.dm new file mode 100644 index 0000000000..c05a54da64 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/Switch/weird_when_clause_pragma.dm @@ -0,0 +1,16 @@ +//COMPILE ERROR +//test to make sure SuspiciousSwitchCase is working + +#pragma SuspiciousSwitchCase error + +/proc/RunTest() + var/x = 5 + switch(x) + if(1) + return + if(4) + return + else if(x == 3) + return + else + return diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 7aa20b1258..d9c2554a89 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -8,14 +8,9 @@ namespace DMCompiler.Compiler.DM { public partial class DMParser : Parser { - private DreamPath _currentPath = DreamPath.Root; - private bool _allowVarDeclExpression = false; - public DMParser(DMLexer lexer) : base(lexer) { - } - private static readonly TokenType[] AssignTypes = { TokenType.DM_Equals, TokenType.DM_PlusEquals, @@ -149,6 +144,9 @@ public DMParser(DMLexer lexer) : base(lexer) { TokenType.DM_XorEquals, }; + public DMParser(DMLexer lexer) : base(lexer) { + } + public DMASTFile File() { var loc = Current().Location; List statements = new(); @@ -1301,7 +1299,7 @@ DMASTProcBlockInner GetForBody() { DMASTProcStatementSwitch.SwitchCase[]? switchCases = SwitchCases(); - if (switchCases == null) Error("Expected switch cases"); + if (switchCases == null) Error(WarningCode.BadExpression, "Expected switch cases"); return new DMASTProcStatementSwitch(loc, value, switchCases); } @@ -1352,7 +1350,7 @@ public DMASTProcStatementSwitch.SwitchCase[] SwitchInner() { List switchCases = new(); DMASTProcStatementSwitch.SwitchCase? switchCase = SwitchCase(); - while (switchCase != null) { + while (switchCase is not null) { switchCases.Add(switchCase); Newline(); Whitespace(); @@ -1374,19 +1372,20 @@ public DMASTProcStatementSwitch.SwitchCase[] SwitchInner() { DMASTExpression? expression = Expression(); if (expression == null) { - if (expressions.Count == 0) { - Error("Expected an expression"); - } else { - //Eat a trailing comma if there's at least 1 expression - break; - } + if (expressions.Count == 0) + DMCompiler.Emit(WarningCode.BadExpression, Current().Location, "Expected an expression"); + + break; } if (Check(TokenType.DM_To)) { - var loc = Current().Location; Whitespace(); + var loc = Current().Location; DMASTExpression? rangeEnd = Expression(); - if (rangeEnd == null) Error("Expected an upper limit"); + if (rangeEnd == null) { + DMCompiler.Emit(WarningCode.BadExpression, loc, "Expected an upper limit"); + rangeEnd = new DMASTConstantNull(loc); // Fallback to null + } expressions.Add(new DMASTSwitchCaseRange(loc, expression, rangeEnd)); } else { @@ -1402,22 +1401,25 @@ public DMASTProcStatementSwitch.SwitchCase[] SwitchInner() { if (body == null) { DMASTProcStatement? statement = ProcStatement(); - var loc = Current().Location; if (statement != null) { - body = new DMASTProcBlockInner(loc,statement); + body = new DMASTProcBlockInner(statement.Location, statement); } else { - body = new DMASTProcBlockInner(loc); + body = new DMASTProcBlockInner(Current().Location); } } return new DMASTProcStatementSwitch.SwitchCaseValues(expressions.ToArray(), body); } else if (Check(TokenType.DM_Else)) { - var loc = Current().Location; Whitespace(); + var loc = Current().Location; if (Current().Type == TokenType.DM_If) { - Error("Expected \"if\" or \"else\", \"else if\" is not permitted as a switch case"); + //From now on, all ifs/elseifs/elses are actually part of this if's chain, not the switch's. + //Ambiguous, but that is parity behaviour. Ergo, the following emission. + DMCompiler.Emit(WarningCode.SuspiciousSwitchCase, loc, + "Expected \"if\" or \"else\" - \"else if\" is ambiguous as a switch case and may cause unintended flow"); } + DMASTProcBlockInner? body = ProcBlock(); if (body == null) { diff --git a/DMCompiler/Compiler/DM/DMParserHelper.cs b/DMCompiler/Compiler/DM/DMParserHelper.cs index 03f417875f..9acef9c83e 100644 --- a/DMCompiler/Compiler/DM/DMParserHelper.cs +++ b/DMCompiler/Compiler/DM/DMParserHelper.cs @@ -1,5 +1,4 @@ using OpenDreamShared.Compiler; -using DMCompiler.Compiler.DMPreprocessor; using System; using System.Collections.Generic; using System.Linq; @@ -7,8 +6,7 @@ using DMCompiler.Bytecode; namespace DMCompiler.Compiler.DM { - public partial class DMParser : Parser { - + public partial class DMParser { /// /// A special override of Error() since, for DMParser, we know we are in a compilation context and can make use of error codes. /// @@ -23,6 +21,12 @@ protected bool Error(WarningCode code, string message) { return level == ErrorLevel.Error; } + /// + [Obsolete("This is not a desirable way for DMParser to emit an error, as errors should emit an error code and not cause unnecessary throws. Use DMParser's overrides of this method, instead.")] + new protected void Error(string message, bool throwException = true) { + base.Error(message, throwException); + } + protected bool PeekDelimiter() { return Current().Type == TokenType.Newline || Current().Type == TokenType.DM_Semicolon; } diff --git a/DMCompiler/DMStandard/DefaultPragmaConfig.dm b/DMCompiler/DMStandard/DefaultPragmaConfig.dm index 2c71d6c4e4..e256ae41a6 100644 --- a/DMCompiler/DMStandard/DefaultPragmaConfig.dm +++ b/DMCompiler/DMStandard/DefaultPragmaConfig.dm @@ -27,6 +27,7 @@ #pragma DanglingVarType warning #pragma MissingInterpolatedExpression warning #pragma AmbiguousResourcePath warning +#pragma SuspiciousSwitchCase warning //3000-3999 #pragma EmptyBlock notice diff --git a/OpenDreamShared/Compiler/CompilerError.cs b/OpenDreamShared/Compiler/CompilerError.cs index 31a9fa93b7..10b15cb7c9 100644 --- a/OpenDreamShared/Compiler/CompilerError.cs +++ b/OpenDreamShared/Compiler/CompilerError.cs @@ -57,7 +57,8 @@ public enum WarningCode { // 3000 - 3999 are reserved for stylistic configuration. EmptyBlock = 3100, EmptyProc = 3101, - UnsafeClientAccess = 3200 + UnsafeClientAccess = 3200, + SuspiciousSwitchCase = 3201, // "else if" cases are actually valid DM, they just spontaneously end the switch context and begin an if-else ladder within the else case of the switch // 4000 - 4999 are reserved for runtime configuration. (TODO: Runtime doesn't know about configs yet!) } From b6375daabca253d1290581a70cf4178072215e5f Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 20 Dec 2023 17:50:04 -0500 Subject: [PATCH 16/64] Attempt to reconnect after a disconnect (#1523) * Attempt to reconnect after a timed out connection * Update OpenDreamClient/States/DreamUserInterfaceStateManager.cs --- .../States/DreamUserInterfaceStateManager.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/OpenDreamClient/States/DreamUserInterfaceStateManager.cs b/OpenDreamClient/States/DreamUserInterfaceStateManager.cs index 609e7b29c6..47b2e7343d 100644 --- a/OpenDreamClient/States/DreamUserInterfaceStateManager.cs +++ b/OpenDreamClient/States/DreamUserInterfaceStateManager.cs @@ -31,8 +31,14 @@ public void Initialize() { // When we disconnect from the server: case ClientRunLevel.Error: case ClientRunLevel.Initialize when args.OldLevel >= ClientRunLevel.Connected: - if (_gameController.LaunchState.FromLauncher) { - _stateManager.RequestStateChange(); + // TODO: Reconnect without returning to the launcher + // The client currently believes its still connected at this point and will refuse + if (_gameController.LaunchState is { + FromLauncher: true, + Ss14Address: not null + }) { + _gameController.Redial(_gameController.LaunchState.Ss14Address, "Connection lost; attempting reconnect"); + break; } From a3dcd596ff1db5cc06299fe1eee8ce3ebd679fd5 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 20 Dec 2023 21:00:29 -0500 Subject: [PATCH 17/64] Fix `new /turf(loc)` not updating name and desc (#1559) --- OpenDreamRuntime/AtomManager.cs | 7 ------- OpenDreamRuntime/Objects/DreamObjectTree.cs | 1 + OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs | 11 +++++++---- OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs | 12 ++++++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index c5bb012ac7..61e3230fd2 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -192,13 +192,6 @@ public EntityUid CreateMovableEntity(DreamObjectMovable movable) { DMISpriteComponent sprite = _entityManager.AddComponent(entity); sprite.SetAppearance(GetAppearanceFromDefinition(movable.ObjectDefinition)); - if (_entityManager.TryGetComponent(entity, out MetaDataComponent? metaData)) { - _metaDataSystem ??= _entitySystemManager.GetEntitySystem(); - - _metaDataSystem.SetEntityName(entity, movable.GetDisplayName(), metaData); - _metaDataSystem.SetEntityDescription(entity, movable.Desc ?? string.Empty, metaData); - } - _entityToAtom.Add(entity, movable); return entity; } diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index 22f19fbccd..dcb884708f 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -76,6 +76,7 @@ public void LoadJson(DreamCompiledJson json) { _entitySystemManager.TryGetEntitySystem(out _appearanceSystem); _entitySystemManager.TryGetEntitySystem(out _transformSystem); _entitySystemManager.TryGetEntitySystem(out _pvsOverrideSystem); + _entitySystemManager.TryGetEntitySystem(out _metaDataSystem); Strings = json.Strings ?? new(); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index 988be375f5..a5f413278f 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -1,4 +1,5 @@ -using OpenDreamShared.Dream; +using OpenDreamRuntime.Procs; +using OpenDreamShared.Dream; namespace OpenDreamRuntime.Objects.Types; @@ -14,9 +15,6 @@ public class DreamObjectAtom : DreamObject { public DreamList? VisLocs; // TODO: Implement public DreamObjectAtom(DreamObjectDefinition objectDefinition) : base(objectDefinition) { - ObjectDefinition.Variables["name"].TryGetValueAsString(out Name); - ObjectDefinition.Variables["desc"].TryGetValueAsString(out Desc); - Overlays = new(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, false); Underlays = new(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, true); VisContents = new(ObjectTree.List.ObjectDefinition, PvsOverrideSystem, this); @@ -26,6 +24,11 @@ public DreamObjectAtom(DreamObjectDefinition objectDefinition) : base(objectDefi AtomManager.AddAtom(this); } + public override void Initialize(DreamProcArguments args) { + ObjectDefinition.Variables["name"].TryGetValueAsString(out Name); + ObjectDefinition.Variables["desc"].TryGetValueAsString(out Desc); + } + protected override void HandleDeletion() { AtomManager.RemoveAtom(this); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs index d0cff5a599..a24a19d891 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs @@ -2,7 +2,6 @@ using OpenDreamRuntime.Rendering; using OpenDreamShared.Dream; using Robust.Shared.Map; -using Robust.Shared.Utility; namespace OpenDreamRuntime.Objects.Types; @@ -40,14 +39,19 @@ public DreamObjectMovable(DreamObjectDefinition objectDefinition) : base(objectD Entity = AtomManager.CreateMovableEntity(this); SpriteComponent = EntityManager.GetComponent(Entity); _transformComponent = EntityManager.GetComponent(Entity); - - objectDefinition.Variables["screen_loc"].TryGetValueAsString(out var screenLoc); - ScreenLoc = screenLoc; } public override void Initialize(DreamProcArguments args) { base.Initialize(args); + ObjectDefinition.Variables["screen_loc"].TryGetValueAsString(out var screenLoc); + ScreenLoc = screenLoc; + + if (EntityManager.TryGetComponent(Entity, out MetaDataComponent? metaData)) { + MetaDataSystem?.SetEntityName(Entity, GetDisplayName(), metaData); + MetaDataSystem?.SetEntityDescription(Entity, Desc ?? string.Empty, metaData); + } + args.GetArgument(0).TryGetValueAsDreamObject(out var loc); SetLoc(loc); //loc is set before /New() is ever called } From c7e8d71edbbc7525b2b2cd3521f8965a07a3b63c Mon Sep 17 00:00:00 2001 From: distributivgesetz Date: Sat, 23 Dec 2023 22:11:05 +0100 Subject: [PATCH 18/64] Move duplicate opcode check to a unit test (#1563) * move opcode unique validity check to unit tests * automatic code style changes :/ --- Content.Tests/RuntimeTests.cs | 16 ++++++++++++++++ DMCompiler/Bytecode/DreamProcOpcode.cs | 21 --------------------- DMCompiler/DMCompiler.cs | 10 +++------- 3 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 Content.Tests/RuntimeTests.cs diff --git a/Content.Tests/RuntimeTests.cs b/Content.Tests/RuntimeTests.cs new file mode 100644 index 0000000000..81e6e9f46e --- /dev/null +++ b/Content.Tests/RuntimeTests.cs @@ -0,0 +1,16 @@ +using System; +using DMCompiler.Bytecode; +using NUnit.Framework; + +namespace Content.Tests; + +[TestFixture] +public sealed class RuntimeTests { + /// + /// Validates that the opcodes in DreamProcOpcode are all unique, such that none resolve to the same byte. + /// + [Test] + public void EnsureOpcodesUnique() { + Assert.That(Enum.GetValues(), Is.Unique); + } +} diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index 10bb98d707..766f8b9f41 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -363,27 +363,6 @@ public override string ToString() { // Dummy class-as-namespace because C# just kinda be like this public static class OpcodeVerifier { - /// - /// Validates that the opcodes in DreamProcOpcode are all unique, such that none resolve to the same byte. - /// - /// True if there are duplicate opcodes, false if not - // FIXME: Can this be made into something done during compiletime? Like, *this code's* compiletime? >:/ - public static bool AreOpcodesInvalid() { - // I'm not *too* satisfied with this boolean schtick, as opposed to throwing, - // but I want each executable to be able to do what they want with this information. - - HashSet bytePool = new(); - foreach (DreamProcOpcode usedInt in Enum.GetValues(typeof(DreamProcOpcode))) { - if(bytePool.Contains(usedInt)) { - return true; - } - - bytePool.Add(usedInt); - } - - return false; - } - /// /// Calculates a hash of all the DreamProcOpcodes for warning on incompatibilities. /// diff --git a/DMCompiler/DMCompiler.cs b/DMCompiler/DMCompiler.cs index 823d43ee7c..ab396e178a 100644 --- a/DMCompiler/DMCompiler.cs +++ b/DMCompiler/DMCompiler.cs @@ -1,3 +1,4 @@ +using DMCompiler.Bytecode; using DMCompiler.Compiler.DM; using DMCompiler.Compiler.DMM; using DMCompiler.Compiler.DMPreprocessor; @@ -5,17 +6,16 @@ using DMCompiler.DM.Visitors; using OpenDreamShared.Compiler; using OpenDreamShared.Json; +using Robust.Shared.Utility; using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Reflection; using System.Linq; +using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using DMCompiler.Bytecode; -using Robust.Shared.Utility; namespace DMCompiler; @@ -51,10 +51,6 @@ public static bool Compile(DMCompilerSettings settings) { ForcedWarning("Unimplemented proc & var warnings are currently suppressed"); } - if(OpcodeVerifier.AreOpcodesInvalid()) { - ForcedError("Some opcodes have the same byte value! Output assembly may be corrupted."); - } - DMPreprocessor preprocessor = Preprocess(settings.Files, settings.MacroDefines); bool successfulCompile = preprocessor is not null && Compile(preprocessor); From 86a56cc33ea713013a07c040e041a3e06a6369f1 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 24 Dec 2023 13:33:22 -0500 Subject: [PATCH 19/64] TGS6 is mainline now, fix integration test target (#1566) --- .github/workflows/test-tgs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-tgs.yml b/.github/workflows/test-tgs.yml index e87fc6b13c..a901f1dc02 100644 --- a/.github/workflows/test-tgs.yml +++ b/.github/workflows/test-tgs.yml @@ -13,7 +13,7 @@ concurrency: env: OD_DOTNET_VERSION: 7 TGS_DOTNET_VERSION: 8 - TGS_REFERENCE: V6 # This will break after the V6 branch gets deleted during the TGS6 release cycle. When it does, change this to `dev` + TGS_REFERENCE: dev jobs: build: From 2bd8710ef0f51b4791985629161a34cb44746be6 Mon Sep 17 00:00:00 2001 From: wixoa Date: Mon, 25 Dec 2023 23:26:37 -0500 Subject: [PATCH 20/64] Update to RT v195.0.1 and .NET 8 (#1562) * Update to RT v194.1.0 and .NET 8 * Update workflows to .NET 8 * Update .NET version in README.md * Some file-scoped namespaces * Reduce allocations in the renderer using a new feature You can pass an existing HashSet to `GetEntitiesIntersecting()` now * Re-add global.json * Update to RT v195.0.1 --- .github/workflows/build-test.yml | 2 +- .github/workflows/compiler-test.yml | 2 +- .github/workflows/test-tgs.yml | 2 +- .../Content.IntegrationTests.csproj | 2 +- Content.Tests/Content.Tests.csproj | 2 +- DMCompiler/DMCompiler.csproj | 2 +- DMCompiler/copy_standard.bat | 8 +- DMCompiler/copy_standard.sh | 16 +- DMDisassembler/DMDisassembler.csproj | 2 +- OpenDreamClient/Audio/DreamSoundChannel.cs | 18 +- OpenDreamClient/Audio/DreamSoundEngine.cs | 128 +-- OpenDreamClient/DreamClientSystem.cs | 2 +- OpenDreamClient/OpenDreamClient.csproj | 2 +- OpenDreamClient/Rendering/DreamViewOverlay.cs | 8 +- .../Resources/ResourceTypes/ResourceSound.cs | 44 +- .../OpenDreamPackageTool.csproj | 2 +- OpenDreamPackaging/DreamPackaging.cs | 6 +- OpenDreamPackaging/OpenDreamPackaging.csproj | 2 +- OpenDreamRuntime/DreamConnection.cs | 903 +++++++++--------- OpenDreamRuntime/Objects/Types/DreamList.cs | 2 +- .../Objects/Types/DreamObjectMovable.cs | 4 +- OpenDreamRuntime/OpenDreamRuntime.csproj | 2 +- OpenDreamServer/OpenDreamServer.csproj | 2 +- OpenDreamShared/OpenDreamShared.csproj | 2 +- README.md | 2 +- RobustToolbox | 2 +- global.json | 2 +- 27 files changed, 572 insertions(+), 599 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 99dc929e41..d6dc02d690 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,7 +28,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.101 + dotnet-version: 8.0.100 - name: Install dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/compiler-test.yml b/.github/workflows/compiler-test.yml index 329007ebf9..95e70668c1 100644 --- a/.github/workflows/compiler-test.yml +++ b/.github/workflows/compiler-test.yml @@ -27,7 +27,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.101 + dotnet-version: 8.0.100 - name: Install dependencies run: dotnet restore main/OpenDream.sln - name: Build diff --git a/.github/workflows/test-tgs.yml b/.github/workflows/test-tgs.yml index a901f1dc02..a6a34ad7b4 100644 --- a/.github/workflows/test-tgs.yml +++ b/.github/workflows/test-tgs.yml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true env: - OD_DOTNET_VERSION: 7 + OD_DOTNET_VERSION: 8 TGS_DOTNET_VERSION: 8 TGS_REFERENCE: dev diff --git a/Content.IntegrationTests/Content.IntegrationTests.csproj b/Content.IntegrationTests/Content.IntegrationTests.csproj index 78418cb1b2..71b32f8827 100644 --- a/Content.IntegrationTests/Content.IntegrationTests.csproj +++ b/Content.IntegrationTests/Content.IntegrationTests.csproj @@ -5,7 +5,7 @@ ..\bin\Content.IntegrationTests\ false false - 11 + 12 diff --git a/Content.Tests/Content.Tests.csproj b/Content.Tests/Content.Tests.csproj index 9b757865e1..719446e02d 100644 --- a/Content.Tests/Content.Tests.csproj +++ b/Content.Tests/Content.Tests.csproj @@ -2,7 +2,7 @@ $(TargetFramework) - 11 + 12 false false ..\bin\Content.Tests\ diff --git a/DMCompiler/DMCompiler.csproj b/DMCompiler/DMCompiler.csproj index c533fdee4f..53b82d0b6c 100644 --- a/DMCompiler/DMCompiler.csproj +++ b/DMCompiler/DMCompiler.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable Debug;Release;Tools AnyCPU diff --git a/DMCompiler/copy_standard.bat b/DMCompiler/copy_standard.bat index d57f678a2a..d53a132c2d 100644 --- a/DMCompiler/copy_standard.bat +++ b/DMCompiler/copy_standard.bat @@ -1,5 +1,5 @@ @echo off -if not exist bin\Debug\net7.0\DMStandard mkdir bin\Debug\net7.0\DMStandard -xcopy DMStandard bin\Debug\net7.0\DMStandard /y /s /e -if not exist bin\Release\net7.0\DMStandard mkdir bin\Release\net7.0\DMStandard -xcopy DMStandard bin\Release\net7.0\DMStandard /y /s /e +if not exist bin\Debug\net8.0\DMStandard mkdir bin\Debug\net8.0\DMStandard +xcopy DMStandard bin\Debug\net8.0\DMStandard /y /s /e +if not exist bin\Release\net8.0\DMStandard mkdir bin\Release\net8.0\DMStandard +xcopy DMStandard bin\Release\net8.0\DMStandard /y /s /e diff --git a/DMCompiler/copy_standard.sh b/DMCompiler/copy_standard.sh index d1f8bee8ef..3532579229 100755 --- a/DMCompiler/copy_standard.sh +++ b/DMCompiler/copy_standard.sh @@ -4,16 +4,16 @@ set -xe SCRIPT=$(readlink -f "$0") SCRIPTPATH=$(dirname "$SCRIPT") -if [ -d "$SCRIPTPATH/bin/Debug/net7.0/DMStandard" ]; then - cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Debug/net7.0/DMStandard +if [ -d "$SCRIPTPATH/bin/Debug/net8.0/DMStandard" ]; then + cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Debug/net8.0/DMStandard else - mkdir -p $SCRIPTPATH/bin/Debug/net7.0/DMStandard - cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Debug/net7.0/DMStandard + mkdir -p $SCRIPTPATH/bin/Debug/net8.0/DMStandard + cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Debug/net8.0/DMStandard fi -if [ -d "$SCRIPTPATH/bin/Release/net7.0/DMStandard" ]; then - cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Release/net7.0/DMStandard +if [ -d "$SCRIPTPATH/bin/Release/net8.0/DMStandard" ]; then + cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Release/net8.0/DMStandard else - mkdir -p $SCRIPTPATH/bin/Release/net7.0/DMStandard - cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Release/net7.0/DMStandard + mkdir -p $SCRIPTPATH/bin/Release/net8.0/DMStandard + cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Release/net8.0/DMStandard fi diff --git a/DMDisassembler/DMDisassembler.csproj b/DMDisassembler/DMDisassembler.csproj index 231be680b8..e116fcf4e6 100644 --- a/DMDisassembler/DMDisassembler.csproj +++ b/DMDisassembler/DMDisassembler.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 Debug;Release;Tools AnyCPU diff --git a/OpenDreamClient/Audio/DreamSoundChannel.cs b/OpenDreamClient/Audio/DreamSoundChannel.cs index 45ab8a4ec9..525882e14c 100644 --- a/OpenDreamClient/Audio/DreamSoundChannel.cs +++ b/OpenDreamClient/Audio/DreamSoundChannel.cs @@ -1,20 +1,12 @@ -using Robust.Client.Graphics; +using Robust.Client.Audio; +using Robust.Shared.Audio.Components; namespace OpenDreamClient.Audio; -public sealed class DreamSoundChannel : IDisposable { - public IClydeAudioSource Source { get; } - - public DreamSoundChannel(IClydeAudioSource source) { - Source = source; - } +public sealed class DreamSoundChannel(AudioSystem audioSystem, (EntityUid Entity, AudioComponent Component) source) { + public readonly (EntityUid Entity, AudioComponent Component) Source = source; public void Stop() { - Source.StopPlaying(); - } - - public void Dispose() { - Stop(); - Source.Dispose(); + audioSystem.Stop(Source.Entity, Source.Component); } } diff --git a/OpenDreamClient/Audio/DreamSoundEngine.cs b/OpenDreamClient/Audio/DreamSoundEngine.cs index 703bd8ca35..8665d3f751 100644 --- a/OpenDreamClient/Audio/DreamSoundEngine.cs +++ b/OpenDreamClient/Audio/DreamSoundEngine.cs @@ -1,89 +1,103 @@ using OpenDreamClient.Resources; using OpenDreamClient.Resources.ResourceTypes; using OpenDreamShared.Network.Messages; -using Robust.Client.Graphics; +using Robust.Client.Audio; using Robust.Shared.Audio; using Robust.Shared.Network; -namespace OpenDreamClient.Audio { - public sealed class DreamSoundEngine : IDreamSoundEngine { - private const int SoundChannelLimit = 1024; +namespace OpenDreamClient.Audio; - [Dependency] private readonly IDreamResourceManager _resourceManager = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly INetManager _netManager = default!; +public sealed class DreamSoundEngine : IDreamSoundEngine { + private const int SoundChannelLimit = 1024; - private ISawmill _sawmill = default!; + [Dependency] private readonly IDreamResourceManager _resourceManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + [Dependency] private readonly IAudioManager _audioManager = default!; + private AudioSystem? _audioSystem; - private readonly DreamSoundChannel?[] _channels = new DreamSoundChannel[SoundChannelLimit]; + private ISawmill _sawmill = default!; - public void Initialize() { - _sawmill = _logManager.GetSawmill("opendream.audio"); + private readonly DreamSoundChannel?[] _channels = new DreamSoundChannel[SoundChannelLimit]; - _netManager.RegisterNetMessage(RxSound); + public void Initialize() { + _sawmill = _logManager.GetSawmill("opendream.audio"); - _netManager.Disconnect += DisconnectedFromServer; - } + _netManager.RegisterNetMessage(RxSound); - public void StopFinishedChannels() { - for (int i = 0; i < SoundChannelLimit; i++) { - if (_channels[i]?.Source.IsPlaying is false or null) - StopChannel(i + 1); - } + _netManager.Disconnect += DisconnectedFromServer; + } + + public void StopFinishedChannels() { + for (int i = 0; i < SoundChannelLimit; i++) { + if (_channels[i]?.Source.Component.Playing is false or null) + StopChannel(i + 1); } + } - public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume) { - if (channel == 0) { - //First available channel - for (int i = 0; i < _channels.Length; i++) { - if (_channels[i] == null) { - channel = i + 1; - break; - } - } + public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume) { + if (_audioSystem == null) + _entitySystemManager.Resolve(ref _audioSystem); - if (channel == 0) { - _sawmill.Error("Failed to find a free audio channel to play a sound on"); - return; + if (channel == 0) { + //First available channel + for (int i = 0; i < _channels.Length; i++) { + if (_channels[i] == null) { + channel = i + 1; + break; } } - StopChannel(channel); - - // convert from DM volume (0-100) to OpenAL volume (db) - IClydeAudioSource? source = sound.Play(format, AudioParams.Default.WithVolume(20 * MathF.Log10(volume))); - if (source == null) + if (channel == 0) { + _sawmill.Error("Failed to find a free audio channel to play a sound on"); return; - - _channels[channel - 1] = new DreamSoundChannel(source); + } } + StopChannel(channel); - public void StopChannel(int channel) { - ref DreamSoundChannel? ch = ref _channels[channel - 1]; - - ch?.Dispose(); - // This will null the corresponding index in the array. - ch = null; + var stream = sound.GetStream(format, _audioManager); + if (stream == null) { + _sawmill.Error($"Failed to load audio ${sound}"); + return; } - public void StopAllChannels() { - for (int i = 0; i < SoundChannelLimit; i++) { - StopChannel(i + 1); - } + var db = 20 * MathF.Log10(volume); // convert from DM volume (0-100) to OpenAL volume (db) + var source = _audioSystem.PlayGlobal(stream, AudioParams.Default.WithVolume(db)); // TODO: Positional audio. + if (source == null) { + _sawmill.Error($"Failed to play audio ${sound}"); + return; } - private void RxSound(MsgSound msg) { - if (msg.ResourceId.HasValue) { - _resourceManager.LoadResourceAsync(msg.ResourceId.Value, - sound => PlaySound(msg.Channel, msg.Format!.Value, sound, msg.Volume / 100.0f)); - } else { - StopChannel(msg.Channel); - } + _channels[channel - 1] = new DreamSoundChannel(_audioSystem, source.Value); + } + + + public void StopChannel(int channel) { + ref DreamSoundChannel? ch = ref _channels[channel - 1]; + + ch?.Stop(); + // This will null the corresponding index in the array. + ch = null; + } + + public void StopAllChannels() { + for (int i = 0; i < SoundChannelLimit; i++) { + StopChannel(i + 1); } + } - private void DisconnectedFromServer(object? sender, NetDisconnectedArgs e) { - StopAllChannels(); + private void RxSound(MsgSound msg) { + if (msg.ResourceId.HasValue) { + _resourceManager.LoadResourceAsync(msg.ResourceId.Value, + sound => PlaySound(msg.Channel, msg.Format!.Value, sound, msg.Volume / 100.0f)); + } else { + StopChannel(msg.Channel); } } + + private void DisconnectedFromServer(object? sender, NetDisconnectedArgs e) { + StopAllChannels(); + } } diff --git a/OpenDreamClient/DreamClientSystem.cs b/OpenDreamClient/DreamClientSystem.cs index c6de6edfd6..029c7ca44b 100644 --- a/OpenDreamClient/DreamClientSystem.cs +++ b/OpenDreamClient/DreamClientSystem.cs @@ -2,7 +2,7 @@ using OpenDreamClient.Rendering; using Robust.Client.GameObjects; using Robust.Client.Graphics; -using Robust.Client.Player; +using Robust.Shared.Player; namespace OpenDreamClient; diff --git a/OpenDreamClient/OpenDreamClient.csproj b/OpenDreamClient/OpenDreamClient.csproj index b741bea1c5..e06241556c 100644 --- a/OpenDreamClient/OpenDreamClient.csproj +++ b/OpenDreamClient/OpenDreamClient.csproj @@ -2,7 +2,7 @@ $(TargetFramework) - 11 + 12 false false ..\bin\Content.Client\ diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index ab2f4a514b..e24977df9a 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -68,6 +68,7 @@ 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, EntityLookupSystem lookupSystem, ClientAppearanceSystem appearanceSystem, ClientScreenOverlaySystem screenOverlaySystem, ClientImagesSystem clientImagesSystem) { @@ -140,19 +141,18 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) var worldHandle = args.WorldHandle; - HashSet entities; 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 - entities = _lookupSystem.GetEntitiesIntersecting(args.MapId, args.WorldAABB.Scale(1.2f), MapLookupFlags); + _lookupSystem.GetEntitiesIntersecting(args.MapId, args.WorldAABB.Scale(1.2f), _entities, MapLookupFlags); } var eyeTile = grid.GetTileRef(eyeTransform.MapPosition); - var tiles = CalculateTileVisibility(grid, entities, eyeTile, seeVis); + var tiles = CalculateTileVisibility(grid, _entities, eyeTile, seeVis); RefreshRenderTargets(args.WorldHandle, viewportSize); - CollectVisibleSprites(tiles, grid, eyeTile, entities, seeVis, sight, args.WorldAABB); + CollectVisibleSprites(tiles, grid, eyeTile, _entities, seeVis, sight, args.WorldAABB); ClearPlanes(); ProcessSprites(worldHandle, viewportSize, args.WorldAABB); diff --git a/OpenDreamClient/Resources/ResourceTypes/ResourceSound.cs b/OpenDreamClient/Resources/ResourceTypes/ResourceSound.cs index c8d36fe471..eb34c92da1 100644 --- a/OpenDreamClient/Resources/ResourceTypes/ResourceSound.cs +++ b/OpenDreamClient/Resources/ResourceTypes/ResourceSound.cs @@ -2,52 +2,28 @@ using JetBrains.Annotations; using OpenDreamShared.Network.Messages; using Robust.Client.Audio; -using Robust.Client.Graphics; -using Robust.Shared.Audio; -namespace OpenDreamClient.Resources.ResourceTypes { - [UsedImplicitly] - public sealed class ResourceSound : DreamResource { - private AudioStream? _stream; +namespace OpenDreamClient.Resources.ResourceTypes; - public ResourceSound(int id, byte[] data) : base(id, data) { } - - public IClydeAudioSource? Play(MsgSound.FormatType format, AudioParams audioParams) { - LoadStream(format); - if (_stream == null) - return null; - - // TODO: Positional audio. - var source = IoCManager.Resolve().CreateAudioSource(_stream); - - if (source != null) { - source.SetGlobal(); - source.SetPitch(audioParams.PitchScale); - source.SetVolume(audioParams.Volume); - source.SetPlaybackPosition(audioParams.PlayOffsetSeconds); - source.IsLooping = audioParams.Loop; - - source.StartPlaying(); - } - - return source; - } - - private void LoadStream(MsgSound.FormatType format) { - if (_stream != null) - return; +[UsedImplicitly] +public sealed class ResourceSound(int id, byte[] data) : DreamResource(id, data) { + private AudioStream? _stream; + public AudioStream? GetStream(MsgSound.FormatType format, IAudioManager audioManager) { + if (_stream == null) { switch (format) { case MsgSound.FormatType.Ogg: - _stream = IoCManager.Resolve().LoadAudioOggVorbis(new MemoryStream(Data)); + _stream = audioManager.LoadAudioOggVorbis(new MemoryStream(Data)); break; case MsgSound.FormatType.Wav: - _stream = IoCManager.Resolve().LoadAudioWav(new MemoryStream(Data)); + _stream = audioManager.LoadAudioWav(new MemoryStream(Data)); break; default: Logger.GetSawmill("opendream.audio").Fatal("Only *.ogg and *.wav audio files are supported."); break; } } + + return _stream; } } diff --git a/OpenDreamPackageTool/OpenDreamPackageTool.csproj b/OpenDreamPackageTool/OpenDreamPackageTool.csproj index 469182ada8..ac86916b7a 100644 --- a/OpenDreamPackageTool/OpenDreamPackageTool.csproj +++ b/OpenDreamPackageTool/OpenDreamPackageTool.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable diff --git a/OpenDreamPackaging/DreamPackaging.cs b/OpenDreamPackaging/DreamPackaging.cs index b26dddae96..13004e1e97 100644 --- a/OpenDreamPackaging/DreamPackaging.cs +++ b/OpenDreamPackaging/DreamPackaging.cs @@ -19,11 +19,9 @@ public static async Task WriteResources( var inputPass = graph.Input; - await RobustClientPackaging.WriteContentAssemblies( - inputPass, + await RobustClientPackaging.WriteClientResources( contentDir, - "Content.Client", - new[] { "OpenDreamClient", "OpenDreamShared" }, + inputPass, cancel); await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel); diff --git a/OpenDreamPackaging/OpenDreamPackaging.csproj b/OpenDreamPackaging/OpenDreamPackaging.csproj index 5797b27310..34748e8ac1 100644 --- a/OpenDreamPackaging/OpenDreamPackaging.csproj +++ b/OpenDreamPackaging/OpenDreamPackaging.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index a750c9da83..8dcf8a6271 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -8,579 +8,572 @@ using OpenDreamShared.Dream; using OpenDreamShared.Dream.Procs; using OpenDreamShared.Network.Messages; -using Robust.Server.GameObjects; using Robust.Shared.Enums; using Robust.Shared.Player; -namespace OpenDreamRuntime { - public sealed class DreamConnection { - [Dependency] private readonly DreamManager _dreamManager = default!; - [Dependency] private readonly DreamObjectTree _objectTree = default!; - [Dependency] private readonly DreamResourceManager _resourceManager = default!; - [Dependency] private readonly WalkManager _walkManager = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - - private readonly ServerScreenOverlaySystem? _screenOverlaySystem; - private readonly ServerClientImagesSystem? _clientImagesSystem; - private readonly ActorSystem? _actorSystem; - - [ViewVariables] private readonly Dictionary _availableVerbs = new(); - [ViewVariables] private readonly Dictionary> _statPanels = new(); - [ViewVariables] private bool _currentlyUpdatingStat; - - [ViewVariables] public ICommonSession? Session { get; private set; } - [ViewVariables] public DreamObjectClient? Client { get; private set; } - [ViewVariables] public DreamObjectMob? Mob { - get => _mob; - set { - if (_mob != value) { - var oldMob = _mob; - _mob = value; - - if (oldMob != null) { - oldMob.Key = null; - oldMob.SpawnProc("Logout"); - oldMob.Connection = null; - } +namespace OpenDreamRuntime; + +public sealed class DreamConnection { + [Dependency] private readonly DreamManager _dreamManager = default!; + [Dependency] private readonly DreamObjectTree _objectTree = default!; + [Dependency] private readonly DreamResourceManager _resourceManager = default!; + [Dependency] private readonly WalkManager _walkManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; + + private readonly ServerScreenOverlaySystem? _screenOverlaySystem; + private readonly ServerClientImagesSystem? _clientImagesSystem; + + [ViewVariables] private readonly Dictionary _availableVerbs = new(); + [ViewVariables] private readonly Dictionary> _statPanels = new(); + [ViewVariables] private bool _currentlyUpdatingStat; + + [ViewVariables] public ICommonSession? Session { get; private set; } + [ViewVariables] public DreamObjectClient? Client { get; private set; } + [ViewVariables] public DreamObjectMob? Mob { + get => _mob; + set { + if (_mob != value) { + var oldMob = _mob; + _mob = value; + + if (oldMob != null) { + oldMob.Key = null; + oldMob.SpawnProc("Logout"); + oldMob.Connection = null; + } - StatObj = new(value); - if (Eye != null && Eye == oldMob) { - Eye = value; - } + StatObj = new(value); + if (Eye != null && Eye == oldMob) { + Eye = value; + } - if (_mob != null) { - // If the mob is already owned by another player, kick them out - if (_mob.Connection != null) - _mob.Connection.Mob = null; + if (_mob != null) { + // If the mob is already owned by another player, kick them out + if (_mob.Connection != null) + _mob.Connection.Mob = null; - _mob.Connection = this; - _mob.Key = Session!.Name; - _mob.SpawnProc("Login", usr: _mob); - } - - UpdateAvailableVerbs(); + _mob.Connection = this; + _mob.Key = Session!.Name; + _mob.SpawnProc("Login", usr: _mob); } + + UpdateAvailableVerbs(); } } + } - [ViewVariables] - public DreamObjectMovable? Eye { - get => _eye; - set { - _eye = value; - - if (_eye != null) { - _actorSystem?.Attach(_eye.Entity, Session!); - } else { - _actorSystem?.Detach(Session!); - } - } + [ViewVariables] + public DreamObjectMovable? Eye { + get => _eye; + set { + _eye = value; + _playerManager.SetAttachedEntity(Session!, _eye?.Entity); } + } - [ViewVariables] - public DreamValue StatObj { get; set; } // This can be just any DreamValue. Only atoms will function though. + [ViewVariables] + public DreamValue StatObj { get; set; } // This can be just any DreamValue. Only atoms will function though. - [ViewVariables] private string? _outputStatPanel; - [ViewVariables] private string _selectedStatPanel; - [ViewVariables] private readonly Dictionary> _promptEvents = new(); - [ViewVariables] private int _nextPromptEvent = 1; + [ViewVariables] private string? _outputStatPanel; + [ViewVariables] private string _selectedStatPanel; + [ViewVariables] private readonly Dictionary> _promptEvents = new(); + [ViewVariables] private int _nextPromptEvent = 1; - private DreamObjectMob? _mob; - private DreamObjectMovable? _eye; + private DreamObjectMob? _mob; + private DreamObjectMovable? _eye; - private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.connection"); + private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.connection"); - public string SelectedStatPanel { - get => _selectedStatPanel; - set { - _selectedStatPanel = value; + public string SelectedStatPanel { + get => _selectedStatPanel; + set { + _selectedStatPanel = value; - var msg = new MsgSelectStatPanel() { StatPanel = value }; - Session?.ConnectedClient.SendMessage(msg); - } + var msg = new MsgSelectStatPanel() { StatPanel = value }; + Session?.ConnectedClient.SendMessage(msg); } + } - public DreamConnection() { - IoCManager.InjectDependencies(this); + public DreamConnection() { + IoCManager.InjectDependencies(this); - _entitySystemManager.TryGetEntitySystem(out _screenOverlaySystem); - _entitySystemManager.TryGetEntitySystem(out _clientImagesSystem); - _entitySystemManager.TryGetEntitySystem(out _actorSystem); - } + _entitySystemManager.TryGetEntitySystem(out _screenOverlaySystem); + _entitySystemManager.TryGetEntitySystem(out _clientImagesSystem); + } - public void HandleConnection(ICommonSession session) { - var client = new DreamObjectClient(_objectTree.Client.ObjectDefinition, this, _screenOverlaySystem, _clientImagesSystem); + public void HandleConnection(ICommonSession session) { + var client = new DreamObjectClient(_objectTree.Client.ObjectDefinition, this, _screenOverlaySystem, _clientImagesSystem); - Session = session; + Session = session; - Client = client; - Client.InitSpawn(new()); + Client = client; + Client.InitSpawn(new()); - SendClientInfoUpdate(); - } + SendClientInfoUpdate(); + } - public void HandleDisconnection() { - if (Session == null || Client == null) // Already disconnected? - return; + public void HandleDisconnection() { + if (Session == null || Client == null) // Already disconnected? + return; - if (_mob != null) { - // Don't null out the ckey here - _mob.SpawnProc("Logout"); + if (_mob != null) { + // Don't null out the ckey here + _mob.SpawnProc("Logout"); - if (_mob != null) { // Logout() may have removed our mob - _mob.Connection = null; - _mob = null; - } + if (_mob != null) { // Logout() may have removed our mob + _mob.Connection = null; + _mob = null; } + } - Client.Delete(); - Client = null; + Client.Delete(); + Client = null; - Session = null; - } + Session = null; + } - public void UpdateAvailableVerbs() { - _availableVerbs.Clear(); - var verbs = new List<(string, string, string)>(); - - void AddVerbs(DreamObject src, IEnumerable adding) { - foreach (DreamValue mobVerb in adding) { - if (!mobVerb.TryGetValueAsProc(out var proc)) - continue; - - string verbName = proc.VerbName ?? proc.Name; - string verbId = verbName.ToLowerInvariant().Replace(" ", "-"); // Case-insensitive, dashes instead of spaces - if (_availableVerbs.ContainsKey(verbId)) { - // BYOND will actually show the user two verbs with different capitalization/dashes, but they will both execute the same verb. - // We make a warning and ignore the latter ones instead. - _sawmill.Warning($"User \"{Session.Name}\" has multiple verb commands named \"{verbId}\", ignoring all but the first"); - continue; - } + public void UpdateAvailableVerbs() { + _availableVerbs.Clear(); + var verbs = new List<(string, string, string)>(); - _availableVerbs.Add(verbId, (src, proc)); + void AddVerbs(DreamObject src, IEnumerable adding) { + foreach (DreamValue mobVerb in adding) { + if (!mobVerb.TryGetValueAsProc(out var proc)) + continue; - // Don't send invisible verbs. - if (_mob != null && proc.Invisibility > _mob.SeeInvisible) { - continue; - } + string verbName = proc.VerbName ?? proc.Name; + string verbId = verbName.ToLowerInvariant().Replace(" ", "-"); // Case-insensitive, dashes instead of spaces + if (_availableVerbs.ContainsKey(verbId)) { + // BYOND will actually show the user two verbs with different capitalization/dashes, but they will both execute the same verb. + // We make a warning and ignore the latter ones instead. + _sawmill.Warning($"User \"{Session.Name}\" has multiple verb commands named \"{verbId}\", ignoring all but the first"); + continue; + } - // Don't send hidden verbs. Names starting with "." count as hidden. - if ((proc.Attributes & ProcAttributes.Hidden) == ProcAttributes.Hidden || - verbName.StartsWith('.')) { - continue; - } + _availableVerbs.Add(verbId, (src, proc)); - string? category = proc.VerbCategory; - // Explicitly null category is hidden from verb panels, "" category becomes the default_verb_category - if (category == string.Empty) { - // But if default_verb_category is null, we hide it from the verb panel - Client.GetVariable("default_verb_category").TryGetValueAsString(out category); - } + // Don't send invisible verbs. + if (_mob != null && proc.Invisibility > _mob.SeeInvisible) { + continue; + } - // Null category is serialized as an empty string and treated as hidden - verbs.Add((verbName, verbId, category ?? string.Empty)); + // Don't send hidden verbs. Names starting with "." count as hidden. + if ((proc.Attributes & ProcAttributes.Hidden) == ProcAttributes.Hidden || + verbName.StartsWith('.')) { + continue; } - } - if (Client != null) { - AddVerbs(Client, Client.Verbs.GetValues()); - } + string? category = proc.VerbCategory; + // Explicitly null category is hidden from verb panels, "" category becomes the default_verb_category + if (category == string.Empty) { + // But if default_verb_category is null, we hide it from the verb panel + Client.GetVariable("default_verb_category").TryGetValueAsString(out category); + } - if (Mob != null) { - AddVerbs(Mob, Mob.Verbs.GetValues()); + // Null category is serialized as an empty string and treated as hidden + verbs.Add((verbName, verbId, category ?? string.Empty)); } + } - var msg = new MsgUpdateAvailableVerbs() { - AvailableVerbs = verbs.ToArray() - }; + if (Client != null) { + AddVerbs(Client, Client.Verbs.GetValues()); + } - Session?.ConnectedClient.SendMessage(msg); + if (Mob != null) { + AddVerbs(Mob, Mob.Verbs.GetValues()); } - public void UpdateStat() { - if (Session == null || Client == null || _currentlyUpdatingStat) - return; + var msg = new MsgUpdateAvailableVerbs() { + AvailableVerbs = verbs.ToArray() + }; - _currentlyUpdatingStat = true; - _statPanels.Clear(); + Session?.ConnectedClient.SendMessage(msg); + } - DreamThread.Run("Stat", async (state) => { - try { - var statProc = Client.GetProc("Stat"); + public void UpdateStat() { + if (Session == null || Client == null || _currentlyUpdatingStat) + return; - await state.Call(statProc, Client, Mob); - if (Session.Status == SessionStatus.InGame) { - var msg = new MsgUpdateStatPanels(_statPanels); - Session.ConnectedClient.SendMessage(msg); - } + _currentlyUpdatingStat = true; + _statPanels.Clear(); - return DreamValue.Null; - } finally { - _currentlyUpdatingStat = false; - } - }); - } + DreamThread.Run("Stat", async (state) => { + try { + var statProc = Client.GetProc("Stat"); - public void SendClientInfoUpdate() { - MsgUpdateClientInfo msg = new() { - View = Client!.View - }; + await state.Call(statProc, Client, Mob); + if (Session.Status == SessionStatus.InGame) { + var msg = new MsgUpdateStatPanels(_statPanels); + Session.ConnectedClient.SendMessage(msg); + } - Session?.ConnectedClient.SendMessage(msg); - } + return DreamValue.Null; + } finally { + _currentlyUpdatingStat = false; + } + }); + } - public void SetOutputStatPanel(string name) { - if (!_statPanels.ContainsKey(name)) - _statPanels.Add(name, new()); + public void SendClientInfoUpdate() { + MsgUpdateClientInfo msg = new() { + View = Client!.View + }; - _outputStatPanel = name; - } + Session?.ConnectedClient.SendMessage(msg); + } - public void AddStatPanelLine(string name, string value, string? atomRef) { - if (_outputStatPanel == null || !_statPanels.ContainsKey(_outputStatPanel)) - SetOutputStatPanel("Stats"); + public void SetOutputStatPanel(string name) { + if (!_statPanels.ContainsKey(name)) + _statPanels.Add(name, new()); - _statPanels[_outputStatPanel].Add( (name, value, atomRef) ); - } + _outputStatPanel = name; + } - public void HandleMsgSelectStatPanel(MsgSelectStatPanel message) { - _selectedStatPanel = message.StatPanel; - } + public void AddStatPanelLine(string name, string value, string? atomRef) { + if (_outputStatPanel == null || !_statPanels.ContainsKey(_outputStatPanel)) + SetOutputStatPanel("Stats"); - public void HandleMsgPromptResponse(MsgPromptResponse message) { - if (!_promptEvents.TryGetValue(message.PromptId, out var promptEvent)) { - _sawmill.Warning($"{message.MsgChannel}: Received MsgPromptResponse for prompt {message.PromptId} which does not exist."); - return; - } + _statPanels[_outputStatPanel].Add( (name, value, atomRef) ); + } - DreamValue value = message.Type switch { - DMValueType.Null => DreamValue.Null, - DMValueType.Text or DMValueType.Message => new DreamValue((string)message.Value), - DMValueType.Num => new DreamValue((float)message.Value), - _ => throw new Exception("Invalid prompt response '" + message.Type + "'") - }; + public void HandleMsgSelectStatPanel(MsgSelectStatPanel message) { + _selectedStatPanel = message.StatPanel; + } - promptEvent.Invoke(value); - _promptEvents.Remove(message.PromptId); + public void HandleMsgPromptResponse(MsgPromptResponse message) { + if (!_promptEvents.TryGetValue(message.PromptId, out var promptEvent)) { + _sawmill.Warning($"{message.MsgChannel}: Received MsgPromptResponse for prompt {message.PromptId} which does not exist."); + return; } - public void HandleMsgTopic(MsgTopic pTopic) { - DreamList hrefList = DreamProcNativeRoot.params2list(_objectTree, HttpUtility.UrlDecode(pTopic.Query)); - DreamValue srcRefValue = hrefList.GetValue(new DreamValue("src")); - DreamValue src = DreamValue.Null; + DreamValue value = message.Type switch { + DMValueType.Null => DreamValue.Null, + DMValueType.Text or DMValueType.Message => new DreamValue((string)message.Value), + DMValueType.Num => new DreamValue((float)message.Value), + _ => throw new Exception("Invalid prompt response '" + message.Type + "'") + }; - if (srcRefValue.TryGetValueAsString(out var srcRef)) { - src = _dreamManager.LocateRef(srcRef); - } + promptEvent.Invoke(value); + _promptEvents.Remove(message.PromptId); + } + + public void HandleMsgTopic(MsgTopic pTopic) { + DreamList hrefList = DreamProcNativeRoot.params2list(_objectTree, HttpUtility.UrlDecode(pTopic.Query)); + DreamValue srcRefValue = hrefList.GetValue(new DreamValue("src")); + DreamValue src = DreamValue.Null; - Client?.SpawnProc("Topic", usr: Mob, new(pTopic.Query), new(hrefList), src); + if (srcRefValue.TryGetValueAsString(out var srcRef)) { + src = _dreamManager.LocateRef(srcRef); } - public void OutputDreamValue(DreamValue value) { - if (value.TryGetValueAsDreamObject(out var outputObject)) { - ushort channel = (ushort)outputObject.GetVariable("channel").GetValueAsInteger(); - ushort volume = (ushort)outputObject.GetVariable("volume").GetValueAsInteger(); - DreamValue file = outputObject.GetVariable("file"); + Client?.SpawnProc("Topic", usr: Mob, new(pTopic.Query), new(hrefList), src); + } - var msg = new MsgSound() { - Channel = channel, - Volume = volume - }; + public void OutputDreamValue(DreamValue value) { + if (value.TryGetValueAsDreamObject(out var outputObject)) { + ushort channel = (ushort)outputObject.GetVariable("channel").GetValueAsInteger(); + ushort volume = (ushort)outputObject.GetVariable("volume").GetValueAsInteger(); + DreamValue file = outputObject.GetVariable("file"); - if (!file.TryGetValueAsDreamResource(out var soundResource)) { - if (file.TryGetValueAsString(out var soundPath)) { - soundResource = _resourceManager.LoadResource(soundPath); - } else if (!file.IsNull) { - throw new ArgumentException($"Cannot output {value}", nameof(value)); - } - } + var msg = new MsgSound() { + Channel = channel, + Volume = volume + }; - msg.ResourceId = soundResource?.Id; - if (soundResource?.ResourcePath is { } resourcePath) { - if (resourcePath.EndsWith(".ogg")) - msg.Format = MsgSound.FormatType.Ogg; - else if (resourcePath.EndsWith(".wav")) - msg.Format = MsgSound.FormatType.Wav; - else - throw new Exception($"Sound {value} is not a supported file type"); + if (!file.TryGetValueAsDreamResource(out var soundResource)) { + if (file.TryGetValueAsString(out var soundPath)) { + soundResource = _resourceManager.LoadResource(soundPath); + } else if (!file.IsNull) { + throw new ArgumentException($"Cannot output {value}", nameof(value)); } + } - Session?.ConnectedClient.SendMessage(msg); - return; + msg.ResourceId = soundResource?.Id; + if (soundResource?.ResourcePath is { } resourcePath) { + if (resourcePath.EndsWith(".ogg")) + msg.Format = MsgSound.FormatType.Ogg; + else if (resourcePath.EndsWith(".wav")) + msg.Format = MsgSound.FormatType.Wav; + else + throw new Exception($"Sound {value} is not a supported file type"); } - OutputControl(value.Stringify(), null); + Session?.ConnectedClient.SendMessage(msg); + return; } - public void OutputControl(string message, string? control) { - var msg = new MsgOutput() { - Value = message, - Control = control - }; + OutputControl(value.Stringify(), null); + } - Session?.ConnectedClient.SendMessage(msg); - } + public void OutputControl(string message, string? control) { + var msg = new MsgOutput() { + Value = message, + Control = control + }; - public void HandleCommand(string fullCommand) { - // TODO: Arguments are a little more complicated than "split by spaces" - // e.g. strings can be passed - string[] args = fullCommand.Split(' ', StringSplitOptions.TrimEntries); - string command = args[0].ToLowerInvariant(); // Case-insensitive - - switch (command) { - case ".north": - case ".east": - case ".south": - case ".west": - case ".northeast": - case ".southeast": - case ".southwest": - case ".northwest": - case ".center": - string movementProc = command switch { - ".north" => "North", - ".east" => "East", - ".south" => "South", - ".west" => "West", - ".northeast" => "Northeast", - ".southeast" => "Southeast", - ".southwest" => "Southwest", - ".northwest" => "Northwest", - ".center" => "Center", - _ => throw new ArgumentOutOfRangeException() - }; - - if (Mob != null) - _walkManager.StopWalks(Mob); - Client?.SpawnProc(movementProc, Mob); break; - - default: { - if (_availableVerbs.TryGetValue(command, out var value)) { - (DreamObject verbSrc, DreamProc verb) = value; - - DreamThread.Run(fullCommand, async (state) => { - DreamValue[] arguments; - if (verb.ArgumentNames != null) { - arguments = new DreamValue[verb.ArgumentNames.Count]; - - // TODO: this should probably be done on the client, shouldn't it? - if (args.Length == 1) { // No args given; prompt the client for them - for (int i = 0; i < verb.ArgumentNames.Count; i++) { - String argumentName = verb.ArgumentNames[i]; - DMValueType argumentType = verb.ArgumentTypes[i]; - DreamValue argumentValue = await Prompt(argumentType, title: String.Empty, // No settable title for verbs - argumentName, defaultValue: String.Empty); // No default value for verbs - - arguments[i] = argumentValue; - } - } else { // Attempt to parse the given arguments - for (int i = 0; i < verb.ArgumentNames.Count; i++) { - DMValueType argumentType = verb.ArgumentTypes[i]; - - if (argumentType == DMValueType.Text) { - arguments[i] = new(args[i+1]); - } else { - _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); - return DreamValue.Null; - } - } - } - } else { - arguments = Array.Empty(); - } + Session?.ConnectedClient.SendMessage(msg); + } - await state.Call(verb, verbSrc, Mob, arguments); - return DreamValue.Null; - }); - } + public void HandleCommand(string fullCommand) { + // TODO: Arguments are a little more complicated than "split by spaces" + // e.g. strings can be passed + string[] args = fullCommand.Split(' ', StringSplitOptions.TrimEntries); + string command = args[0].ToLowerInvariant(); // Case-insensitive + + switch (command) { + case ".north": + case ".east": + case ".south": + case ".west": + case ".northeast": + case ".southeast": + case ".southwest": + case ".northwest": + case ".center": + string movementProc = command switch { + ".north" => "North", + ".east" => "East", + ".south" => "South", + ".west" => "West", + ".northeast" => "Northeast", + ".southeast" => "Southeast", + ".southwest" => "Southwest", + ".northwest" => "Northwest", + ".center" => "Center", + _ => throw new ArgumentOutOfRangeException() + }; - break; - } - } - } + if (Mob != null) + _walkManager.StopWalks(Mob); + Client?.SpawnProc(movementProc, Mob); break; - public Task Prompt(DMValueType types, String title, String message, String defaultValue) { - var task = MakePromptTask(out var promptId); - var msg = new MsgPrompt() { - PromptId = promptId, - Title = title, - Message = message, - Types = types, - DefaultValue = defaultValue - }; + default: { + if (_availableVerbs.TryGetValue(command, out var value)) { + (DreamObject verbSrc, DreamProc verb) = value; - Session.ConnectedClient.SendMessage(msg); - return task; - } + DreamThread.Run(fullCommand, async (state) => { + DreamValue[] arguments; + if (verb.ArgumentNames != null) { + arguments = new DreamValue[verb.ArgumentNames.Count]; - public async Task PromptList(DMValueType types, DreamList list, string title, string message, DreamValue defaultValue) { - List listValues = list.GetValues(); + // TODO: this should probably be done on the client, shouldn't it? + if (args.Length == 1) { // No args given; prompt the client for them + for (int i = 0; i < verb.ArgumentNames.Count; i++) { + String argumentName = verb.ArgumentNames[i]; + DMValueType argumentType = verb.ArgumentTypes[i]; + DreamValue argumentValue = await Prompt(argumentType, title: String.Empty, // No settable title for verbs + argumentName, defaultValue: String.Empty); // No default value for verbs - List promptValues = new(listValues.Count); - for (int i = 0; i < listValues.Count; i++) { - DreamValue value = listValues[i]; + arguments[i] = argumentValue; + } + } else { // Attempt to parse the given arguments + for (int i = 0; i < verb.ArgumentNames.Count; i++) { + DMValueType argumentType = verb.ArgumentTypes[i]; + + if (argumentType == DMValueType.Text) { + arguments[i] = new(args[i+1]); + } else { + _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); + return DreamValue.Null; + } + } + } + } else { + arguments = Array.Empty(); + } - if (types.HasFlag(DMValueType.Obj) && !value.TryGetValueAsDreamObject(out _)) - continue; - if (types.HasFlag(DMValueType.Mob) && !value.TryGetValueAsDreamObject(out _)) - continue; - if (types.HasFlag(DMValueType.Turf) && !value.TryGetValueAsDreamObject(out _)) - continue; - if (types.HasFlag(DMValueType.Area) && !value.TryGetValueAsDreamObject(out _)) - continue; + await state.Call(verb, verbSrc, Mob, arguments); + return DreamValue.Null; + }); + } - promptValues.Add(value.Stringify()); + break; } + } + } - if (promptValues.Count == 0) - return DreamValue.Null; + public Task Prompt(DMValueType types, String title, String message, String defaultValue) { + var task = MakePromptTask(out var promptId); + var msg = new MsgPrompt() { + PromptId = promptId, + Title = title, + Message = message, + Types = types, + DefaultValue = defaultValue + }; + + Session.ConnectedClient.SendMessage(msg); + return task; + } - var task = MakePromptTask(out var promptId); - var msg = new MsgPromptList() { - PromptId = promptId, - Title = title, - Message = message, - CanCancel = (types & DMValueType.Null) == DMValueType.Null, - DefaultValue = defaultValue.Stringify(), - Values = promptValues.ToArray() - }; + public async Task PromptList(DMValueType types, DreamList list, string title, string message, DreamValue defaultValue) { + List listValues = list.GetValues(); - Session.ConnectedClient.SendMessage(msg); + List promptValues = new(listValues.Count); + for (int i = 0; i < listValues.Count; i++) { + DreamValue value = listValues[i]; - // The client returns the index of the selected item, this needs turned back into the DreamValue. - var selectedIndex = await task; - if (selectedIndex.TryGetValueAsInteger(out int index) && index < listValues.Count) { - return listValues[index]; - } + if (types.HasFlag(DMValueType.Obj) && !value.TryGetValueAsDreamObject(out _)) + continue; + if (types.HasFlag(DMValueType.Mob) && !value.TryGetValueAsDreamObject(out _)) + continue; + if (types.HasFlag(DMValueType.Turf) && !value.TryGetValueAsDreamObject(out _)) + continue; + if (types.HasFlag(DMValueType.Area) && !value.TryGetValueAsDreamObject(out _)) + continue; - // Client returned an invalid value. - // Return the first value in the list, or null if cancellable - return msg.CanCancel ? DreamValue.Null : listValues[0]; + promptValues.Add(value.Stringify()); } - public Task WinExists(string controlId) { - var task = MakePromptTask(out var promptId); - var msg = new MsgWinExists() { - PromptId = promptId, - ControlId = controlId - }; + if (promptValues.Count == 0) + return DreamValue.Null; + + var task = MakePromptTask(out var promptId); + var msg = new MsgPromptList() { + PromptId = promptId, + Title = title, + Message = message, + CanCancel = (types & DMValueType.Null) == DMValueType.Null, + DefaultValue = defaultValue.Stringify(), + Values = promptValues.ToArray() + }; + + Session.ConnectedClient.SendMessage(msg); + + // The client returns the index of the selected item, this needs turned back into the DreamValue. + var selectedIndex = await task; + if (selectedIndex.TryGetValueAsInteger(out int index) && index < listValues.Count) { + return listValues[index]; + } - Session.ConnectedClient.SendMessage(msg); + // Client returned an invalid value. + // Return the first value in the list, or null if cancellable + return msg.CanCancel ? DreamValue.Null : listValues[0]; + } - return task; - } + public Task WinExists(string controlId) { + var task = MakePromptTask(out var promptId); + var msg = new MsgWinExists() { + PromptId = promptId, + ControlId = controlId + }; - public Task WinGet(string controlId, string queryValue) { - var task = MakePromptTask(out var promptId); - var msg = new MsgWinGet() { - PromptId = promptId, - ControlId = controlId, - QueryValue = queryValue - }; + Session.ConnectedClient.SendMessage(msg); - Session.ConnectedClient.SendMessage(msg); + return task; + } - return task; - } + public Task WinGet(string controlId, string queryValue) { + var task = MakePromptTask(out var promptId); + var msg = new MsgWinGet() { + PromptId = promptId, + ControlId = controlId, + QueryValue = queryValue + }; - public Task Alert(String title, String message, String button1, String button2, String button3) { - var task = MakePromptTask(out var promptId); - var msg = new MsgAlert() { - PromptId = promptId, - Title = title, - Message = message, - Button1 = button1, - Button2 = button2, - Button3 = button3 - }; + Session.ConnectedClient.SendMessage(msg); - Session.ConnectedClient.SendMessage(msg); - return task; - } + return task; + } - private Task MakePromptTask(out int promptId) { - TaskCompletionSource tcs = new(); - promptId = _nextPromptEvent++; + public Task Alert(String title, String message, String button1, String button2, String button3) { + var task = MakePromptTask(out var promptId); + var msg = new MsgAlert() { + PromptId = promptId, + Title = title, + Message = message, + Button1 = button1, + Button2 = button2, + Button3 = button3 + }; + + Session.ConnectedClient.SendMessage(msg); + return task; + } - _promptEvents.Add(promptId, response => { - tcs.TrySetResult(response); - }); + private Task MakePromptTask(out int promptId) { + TaskCompletionSource tcs = new(); + promptId = _nextPromptEvent++; - return tcs.Task; - } + _promptEvents.Add(promptId, response => { + tcs.TrySetResult(response); + }); - public void BrowseResource(DreamResource resource, string filename) { - if (resource.ResourceData == null) - return; + return tcs.Task; + } - var msg = new MsgBrowseResource() { - Filename = filename, - Data = resource.ResourceData - }; + public void BrowseResource(DreamResource resource, string filename) { + if (resource.ResourceData == null) + return; - Session?.ConnectedClient.SendMessage(msg); - } + var msg = new MsgBrowseResource() { + Filename = filename, + Data = resource.ResourceData + }; - public void Browse(string? body, string? options) { - string? window = null; - Vector2i size = (480, 480); + Session?.ConnectedClient.SendMessage(msg); + } - if (options != null) { - foreach (string option in options.Split(',', ';', '&')) { - string optionTrimmed = option.Trim(); + public void Browse(string? body, string? options) { + string? window = null; + Vector2i size = (480, 480); - if (optionTrimmed != string.Empty) { - string[] optionSeparated = optionTrimmed.Split("=", 2); - string key = optionSeparated[0]; - string value = optionSeparated[1]; + if (options != null) { + foreach (string option in options.Split(',', ';', '&')) { + string optionTrimmed = option.Trim(); - if (key == "window") { - window = value; - } else if (key == "size") { - string[] sizeSeparated = value.Split("x", 2); + if (optionTrimmed != string.Empty) { + string[] optionSeparated = optionTrimmed.Split("=", 2); + string key = optionSeparated[0]; + string value = optionSeparated[1]; - size = (int.Parse(sizeSeparated[0]), int.Parse(sizeSeparated[1])); - } + if (key == "window") { + window = value; + } else if (key == "size") { + string[] sizeSeparated = value.Split("x", 2); + + size = (int.Parse(sizeSeparated[0]), int.Parse(sizeSeparated[1])); } } } - - var msg = new MsgBrowse() { - Size = size, - Window = window, - HtmlSource = body - }; - - Session?.ConnectedClient.SendMessage(msg); } - public void WinSet(string? controlId, string @params) { - var msg = new MsgWinSet() { - ControlId = controlId, - Params = @params - }; + var msg = new MsgBrowse() { + Size = size, + Window = window, + HtmlSource = body + }; - Session?.ConnectedClient.SendMessage(msg); - } + Session?.ConnectedClient.SendMessage(msg); + } - public void WinClone(string controlId, string cloneId) { - var msg = new MsgWinClone() { ControlId = controlId, CloneId = cloneId }; + public void WinSet(string? controlId, string @params) { + var msg = new MsgWinSet() { + ControlId = controlId, + Params = @params + }; - Session?.ConnectedClient.SendMessage(msg); - } + Session?.ConnectedClient.SendMessage(msg); + } - /// - /// Prompts the user to save a file to disk - /// - /// File to save - /// Suggested name to save the file as - public void SendFile(DreamResource file, string suggestedName) { - var msg = new MsgFtp { - ResourceId = file.Id, - SuggestedName = suggestedName - }; + public void WinClone(string controlId, string cloneId) { + var msg = new MsgWinClone() { ControlId = controlId, CloneId = cloneId }; - Session?.ConnectedClient.SendMessage(msg); - } + Session?.ConnectedClient.SendMessage(msg); + } + + /// + /// Prompts the user to save a file to disk + /// + /// File to save + /// Suggested name to save the file as + public void SendFile(DreamResource file, string suggestedName) { + var msg = new MsgFtp { + ResourceId = file.Id, + SuggestedName = suggestedName + }; + + Session?.ConnectedClient.SendMessage(msg); } } diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index eb17f69505..958a001d5d 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -753,7 +753,7 @@ public override void AddValue(DreamValue value) { // TODO: Only override the entity's visibility if its parent atom is visible if (entity != EntityUid.Invalid) - _pvsOverrideSystem?.AddGlobalOverride(entity); + _pvsOverrideSystem?.AddGlobalOverride(_entityManager.GetNetEntity(entity)); _atomManager.UpdateAppearance(_atom, appearance => { // Add even an invalid UID to keep this and _visContents in sync diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs index a24a19d891..0cdd2528fd 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs @@ -84,8 +84,8 @@ protected override bool TryGetVar(string varName, out DreamValue value) { DreamList contents = ObjectTree.CreateList(); using (var childEnumerator = _transformComponent.ChildEnumerator) { - while (childEnumerator.MoveNext(out EntityUid? child)) { - if (!AtomManager.TryGetMovableFromEntity(child.Value, out var childAtom)) + while (childEnumerator.MoveNext(out EntityUid child)) { + if (!AtomManager.TryGetMovableFromEntity(child, out var childAtom)) continue; contents.AddValue(new DreamValue(childAtom)); diff --git a/OpenDreamRuntime/OpenDreamRuntime.csproj b/OpenDreamRuntime/OpenDreamRuntime.csproj index 153b919dee..6c861cf1b5 100644 --- a/OpenDreamRuntime/OpenDreamRuntime.csproj +++ b/OpenDreamRuntime/OpenDreamRuntime.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 enable true Debug;Release;Tools diff --git a/OpenDreamServer/OpenDreamServer.csproj b/OpenDreamServer/OpenDreamServer.csproj index 5f14ea39d0..039555f069 100644 --- a/OpenDreamServer/OpenDreamServer.csproj +++ b/OpenDreamServer/OpenDreamServer.csproj @@ -2,7 +2,7 @@ $(TargetFramework) - 11 + 12 false false ..\bin\Content.Server\ diff --git a/OpenDreamShared/OpenDreamShared.csproj b/OpenDreamShared/OpenDreamShared.csproj index ccb9c13fc0..59d0a5476e 100644 --- a/OpenDreamShared/OpenDreamShared.csproj +++ b/OpenDreamShared/OpenDreamShared.csproj @@ -2,7 +2,7 @@ $(TargetFramework) - 11 + 12 false false ../bin/Content.Shared diff --git a/README.md b/README.md index 85d2fd43ff..03a40d1626 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The first step to building OpenDream is initializing the submodule for the game To do this, simply run `git submodule update --init --recursive` in git bash and let it finish. -**OpenDream requires .NET 7.** You can check your version by running `dotnet --version`. It should be at least `7.0.0`. +**OpenDream requires .NET 8.** You can check your version by running `dotnet --version`. It should be at least `8.0.0`. To build, one can use a C# compiler (such as MSBuild) to compile the various projects described in the solution. diff --git a/RobustToolbox b/RobustToolbox index f5874ea402..eb092e90ef 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit f5874ea402430bb902a5d5d1f47953679d79d781 +Subproject commit eb092e90efc7ac4ae562bc46f9b760745a29e289 diff --git a/global.json b/global.json index 22dfd864b4..3fea262b1b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.0", + "version": "8.0.0", "rollForward": "latestFeature" } } From 1bb067f24c6a542259e09440249e9d799f6fc486 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 26 Dec 2023 01:36:46 -0500 Subject: [PATCH 21/64] Add OpenDreamPackaging DLL to packaging script (#1567) --- OpenDreamPackageTool/ServerPackaging.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OpenDreamPackageTool/ServerPackaging.cs b/OpenDreamPackageTool/ServerPackaging.cs index ee808c2cff..1d38a0939c 100644 --- a/OpenDreamPackageTool/ServerPackaging.cs +++ b/OpenDreamPackageTool/ServerPackaging.cs @@ -33,6 +33,7 @@ public static class ServerPackaging { "OpenDreamServer", "OpenDreamShared", "OpenDreamRuntime", + "OpenDreamPackaging", "Byond.TopicSender", "Microsoft.Extensions.Logging.Abstractions", // dep of Byond.TopicSender "Microsoft.Extensions.DependencyInjection.Abstractions", // dep of above @@ -44,6 +45,7 @@ public static class ServerPackaging { "OpenDreamServer", "OpenDreamShared", "OpenDreamRuntime", + "OpenDreamPackaging", "Byond.TopicSender", "Microsoft.Extensions.Logging.Abstractions", // dep of Byond.TopicSender "Microsoft.Extensions.DependencyInjection.Abstractions", // dep of above From 562f047f03651e9ed817dda526e180262608ce60 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 26 Dec 2023 15:39:08 -0500 Subject: [PATCH 22/64] Make ServerAppearanceSystem thread-safe (#1568) * Use locks on ServerAppearanceSystem * Use a file-scoped namespace --- .../Rendering/ServerAppearanceSystem.cs | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs index abd927ba06..81b96969dd 100644 --- a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs @@ -5,31 +5,40 @@ using System.Diagnostics.CodeAnalysis; using Robust.Shared.Player; -namespace OpenDreamRuntime.Rendering { - public sealed class ServerAppearanceSystem : SharedAppearanceSystem { - private readonly Dictionary _appearanceToId = new(); - private readonly Dictionary _idToAppearance = new(); - private int _appearanceIdCounter; +namespace OpenDreamRuntime.Rendering; - [Dependency] private readonly IPlayerManager _playerManager = default!; +public sealed class ServerAppearanceSystem : SharedAppearanceSystem { + private readonly Dictionary _appearanceToId = new(); + private readonly Dictionary _idToAppearance = new(); + private int _appearanceIdCounter; - public override void Initialize() { - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; - } + /// + /// This system is used by the PVS thread, we need to be thread-safe + /// + private readonly object _lock = new(); + + [Dependency] private readonly IPlayerManager _playerManager = default!; + + public override void Initialize() { + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; + } - public override void Shutdown() { + public override void Shutdown() { + lock (_lock) { _appearanceToId.Clear(); _idToAppearance.Clear(); _appearanceIdCounter = 0; } + } - private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { - if (e.NewStatus == SessionStatus.InGame) { - RaiseNetworkEvent(new AllAppearancesEvent(_idToAppearance), e.Session.ConnectedClient); - } + private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { + if (e.NewStatus == SessionStatus.InGame) { + RaiseNetworkEvent(new AllAppearancesEvent(_idToAppearance), e.Session.ConnectedClient); } + } - public int AddAppearance(IconAppearance appearance) { + public int AddAppearance(IconAppearance appearance) { + lock (_lock) { if (!_appearanceToId.TryGetValue(appearance, out int appearanceId)) { appearanceId = _appearanceIdCounter++; _appearanceToId.Add(appearance, appearanceId); @@ -39,23 +48,29 @@ public int AddAppearance(IconAppearance appearance) { return appearanceId; } + } - public IconAppearance MustGetAppearance(int appearanceId) { + public IconAppearance MustGetAppearance(int appearanceId) { + lock (_lock) { return _idToAppearance[appearanceId]; } + } - public bool TryGetAppearance(int appearanceId, [NotNullWhen(true)] out IconAppearance? appearance) { + public bool TryGetAppearance(int appearanceId, [NotNullWhen(true)] out IconAppearance? appearance) { + lock (_lock) { return _idToAppearance.TryGetValue(appearanceId, out appearance); } + } - public bool TryGetAppearanceId(IconAppearance appearance, out int appearanceId) { + public bool TryGetAppearanceId(IconAppearance appearance, out int appearanceId) { + lock (_lock) { return _appearanceToId.TryGetValue(appearance, out appearanceId); } + } - public void Animate(NetEntity entity, IconAppearance targetAppearance, TimeSpan duration) { - int appearanceId = AddAppearance(targetAppearance); + public void Animate(NetEntity entity, IconAppearance targetAppearance, TimeSpan duration) { + int appearanceId = AddAppearance(targetAppearance); - RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration)); - } + RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration)); } } From 2f97f7ce1df6e144813ad38fb67a379bdce86bb3 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 26 Dec 2023 15:53:07 -0500 Subject: [PATCH 23/64] Put a try/catch around savefile flushing on shutdown (#1569) --- OpenDreamRuntime/EntryPoint.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 7897528a8b..6f962e645e 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -73,7 +73,11 @@ public override void PostInit() { protected override void Dispose(bool disposing) { // Write every savefile to disk foreach (var savefile in DreamObjectSavefile.Savefiles.ToArray()) { //ToArray() to avoid modifying the collection while iterating over it - savefile.Close(); + try { + savefile.Close(); + } catch (Exception e) { + Logger.GetSawmill("opendream").Error($"Exception while flushing savefile '{savefile.Resource.ResourcePath}', data has been lost. {e}"); + } } _dreamManager.Shutdown(); From 79a6dfa1d85d39c6e4252df5acffb22d10fb0a9c Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Wed, 27 Dec 2023 06:23:16 +0000 Subject: [PATCH 24/64] fix next getting set on replcae (#1561) --- Content.Tests/DMProject/Tests/Regex/regex_defer.dm | 3 ++- OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Regex/regex_defer.dm b/Content.Tests/DMProject/Tests/Regex/regex_defer.dm index 19d187e866..b2ca5ed24c 100644 --- a/Content.Tests/DMProject/Tests/Regex/regex_defer.dm +++ b/Content.Tests/DMProject/Tests/Regex/regex_defer.dm @@ -9,4 +9,5 @@ /proc/RunTest() var/regex/R = regex(@"\w+") var/result = R.Replace("Hello, there", /proc/regex_callback) - ASSERT(result == "good, there") \ No newline at end of file + ASSERT(result == "good, there") + ASSERT(R.next == 5) \ No newline at end of file diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs index 83f5440512..9e3b56062d 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRegex.cs @@ -96,8 +96,10 @@ async Task DoProcReplace(AsyncNativeProc.State state, DreamProc proc currentHaystack = regex.Regex.Replace(currentHaystack, replacement, 1, currentStart); currentStart = match.Index + Math.Max(replacement.Length, 1); - if (!regex.IsGlobal) + if (!regex.IsGlobal){ + regex.SetVariable("next", new DreamValue(currentStart + 1)); break; + } } var replaced = currentHaystack; @@ -108,6 +110,12 @@ async Task DoProcReplace(AsyncNativeProc.State state, DreamProc proc } DreamValue DoTextReplace(string replacement) { + if(!regex.IsGlobal) { + var match = regex.Regex.Match(haystackString, Math.Clamp(start - 1, 0, haystackSubstring.Length)); + if (!match.Success) return new DreamValue(haystackString); + regexInstance.SetVariable("next", new DreamValue(match.Index + Math.Max(replacement.Length, 1))); + } + string replaced = regex.Regex.Replace(haystackSubstring, replacement, regex.IsGlobal ? -1 : 1, Math.Clamp(start - 1, 0, haystackSubstring.Length)); From 2792c2dace36a391c4151cd8872328b8503d2b19 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 27 Dec 2023 12:23:21 -0500 Subject: [PATCH 25/64] Optimize `DreamIcon.UpdateAnimation()` (#1572) * Optimize `DreamIcon.UpdateAnimation()` * Some file-scoped namespaces --- .../Rendering/ClientAppearanceSystem.cs | 311 ++++---- .../Rendering/ClientImagesSystem.cs | 42 +- .../Rendering/DMISpriteComponent.cs | 14 +- OpenDreamClient/Rendering/DMISpriteSystem.cs | 17 + OpenDreamClient/Rendering/DreamIcon.cs | 314 ++++---- OpenDreamShared/Resources/DMIParser.cs | 742 +++++++++--------- 6 files changed, 722 insertions(+), 718 deletions(-) diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index 2407ea2036..ba46a9d281 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -5,194 +5,195 @@ using Robust.Shared.Prototypes; using OpenDreamClient.Resources; using OpenDreamClient.Resources.ResourceTypes; +using Robust.Shared.Timing; + +namespace OpenDreamClient.Rendering; + +internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { + private Dictionary _appearances = new(); + private readonly Dictionary>> _appearanceLoadCallbacks = new(); + private readonly Dictionary _turfIcons = new(); + private readonly Dictionary _filterShaders = new(); + + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IDreamResourceManager _dreamResourceManager = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() { + SubscribeNetworkEvent(OnAllAppearances); + SubscribeNetworkEvent(OnNewAppearance); + SubscribeNetworkEvent(OnAnimation); + SubscribeLocalEvent(OnWorldAABB); + } -namespace OpenDreamClient.Rendering { - internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { - private Dictionary _appearances = new(); - private readonly Dictionary>> _appearanceLoadCallbacks = new(); - private readonly Dictionary _turfIcons = new(); - private readonly Dictionary _filterShaders = new(); - - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IDreamResourceManager _dreamResourceManager = default!; - [Dependency] private readonly TransformSystem _transformSystem = default!; - - public override void Initialize() { - SubscribeNetworkEvent(OnAllAppearances); - SubscribeNetworkEvent(OnNewAppearance); - SubscribeNetworkEvent(OnAnimation); - SubscribeLocalEvent(OnWorldAABB); - } + public override void Shutdown() { + _appearances.Clear(); + _appearanceLoadCallbacks.Clear(); + _turfIcons.Clear(); + } - public override void Shutdown() { - _appearances.Clear(); - _appearanceLoadCallbacks.Clear(); - _turfIcons.Clear(); + public void LoadAppearance(int appearanceId, Action loadCallback) { + if (_appearances.TryGetValue(appearanceId, out var appearance)) { + loadCallback(appearance); } - public void LoadAppearance(int appearanceId, Action loadCallback) { - if (_appearances.TryGetValue(appearanceId, out var appearance)) { - loadCallback(appearance); - } - - if (!_appearanceLoadCallbacks.ContainsKey(appearanceId)) { - _appearanceLoadCallbacks.Add(appearanceId, new()); - } - - _appearanceLoadCallbacks[appearanceId].Add(loadCallback); + if (!_appearanceLoadCallbacks.ContainsKey(appearanceId)) { + _appearanceLoadCallbacks.Add(appearanceId, new()); } - public DreamIcon GetTurfIcon(int turfId) { - int appearanceId = turfId - 1; + _appearanceLoadCallbacks[appearanceId].Add(loadCallback); + } - if (!_turfIcons.TryGetValue(appearanceId, out var icon)) { - icon = new DreamIcon(appearanceId); - _turfIcons.Add(appearanceId, icon); - } + public DreamIcon GetTurfIcon(int turfId) { + int appearanceId = turfId - 1; - return icon; + if (!_turfIcons.TryGetValue(appearanceId, out var icon)) { + icon = new DreamIcon(_gameTiming, this, appearanceId); + _turfIcons.Add(appearanceId, icon); } - private void OnAllAppearances(AllAppearancesEvent e, EntitySessionEventArgs session) { - _appearances = e.Appearances; + return icon; + } - foreach (KeyValuePair pair in _appearances) { - if (_appearanceLoadCallbacks.TryGetValue(pair.Key, out var callbacks)) { - foreach (var callback in callbacks) callback(pair.Value); - } + private void OnAllAppearances(AllAppearancesEvent e, EntitySessionEventArgs session) { + _appearances = e.Appearances; + + foreach (KeyValuePair pair in _appearances) { + if (_appearanceLoadCallbacks.TryGetValue(pair.Key, out var callbacks)) { + foreach (var callback in callbacks) callback(pair.Value); } } + } - private void OnNewAppearance(NewAppearanceEvent e) { - _appearances[e.AppearanceId] = e.Appearance; + private void OnNewAppearance(NewAppearanceEvent e) { + _appearances[e.AppearanceId] = e.Appearance; - if (_appearanceLoadCallbacks.TryGetValue(e.AppearanceId, out var callbacks)) { - foreach (var callback in callbacks) callback(e.Appearance); - } + if (_appearanceLoadCallbacks.TryGetValue(e.AppearanceId, out var callbacks)) { + foreach (var callback in callbacks) callback(e.Appearance); } + } - private void OnAnimation(AnimationEvent e) { - EntityUid ent = _entityManager.GetEntity(e.Entity); - if (!_entityManager.TryGetComponent(ent, out var sprite)) - return; + private void OnAnimation(AnimationEvent e) { + EntityUid ent = _entityManager.GetEntity(e.Entity); + if (!_entityManager.TryGetComponent(ent, out var sprite)) + return; - LoadAppearance(e.TargetAppearanceId, targetAppearance => { - sprite.Icon.StartAppearanceAnimation(targetAppearance, e.Duration); - }); - } + LoadAppearance(e.TargetAppearanceId, targetAppearance => { + sprite.Icon.StartAppearanceAnimation(targetAppearance, e.Duration); + }); + } - private void OnWorldAABB(EntityUid uid, DMISpriteComponent comp, ref WorldAABBEvent e) { - Box2? aabb = null; + private void OnWorldAABB(EntityUid uid, DMISpriteComponent comp, ref WorldAABBEvent e) { + Box2? aabb = null; - comp.Icon.GetWorldAABB(_transformSystem.GetWorldPosition(uid), ref aabb); - if (aabb != null) - e.AABB = aabb.Value; - } + comp.Icon.GetWorldAABB(_transformSystem.GetWorldPosition(uid), ref aabb); + if (aabb != null) + e.AABB = aabb.Value; + } - public void ResetFilterUsageFlags() { - foreach (DreamFilter key in _filterShaders.Keys) { - key.Used = false; - } + public void ResetFilterUsageFlags() { + foreach (DreamFilter key in _filterShaders.Keys) { + key.Used = false; } + } - public void CleanUpUnusedFilters() { - foreach (DreamFilter key in _filterShaders.Keys) { - if (!key.Used) - _filterShaders.Remove(key); - } + public void CleanUpUnusedFilters() { + foreach (DreamFilter key in _filterShaders.Keys) { + if (!key.Used) + _filterShaders.Remove(key); } + } - public ShaderInstance GetFilterShader(DreamFilter filter, Dictionary renderSourceLookup) { - if (!_filterShaders.TryGetValue(filter, out var instance)) { - var protoManager = IoCManager.Resolve(); - - instance = protoManager.Index(filter.FilterType).InstanceUnique(); - switch (filter) { - case DreamFilterAlpha alpha: - instance.SetParameter("x",alpha.X); - instance.SetParameter("y",alpha.Y); - instance.SetParameter("flags",alpha.Flags); - break; - case DreamFilterAngularBlur angularBlur: - break; - case DreamFilterBloom bloom: - break; - case DreamFilterBlur blur: - instance.SetParameter("size", blur.Size); - break; - case DreamFilterColor color: { - //Since SWSL doesn't support 4x5 matrices, we need to get a bit silly. - instance.SetParameter("colorMatrix", color.Color.GetMatrix4()); - instance.SetParameter("offsetVector", color.Color.GetOffsetVector()); - //TODO: Support the alternative colour mappings. - break; - } - case DreamFilterDisplace displace: - instance.SetParameter("size", displace.Size); - instance.SetParameter("x", displace.X); - instance.SetParameter("y", displace.Y); - break; - case DreamFilterDropShadow dropShadow: - instance.SetParameter("size", dropShadow.Size); - instance.SetParameter("x", dropShadow.X); - instance.SetParameter("y", dropShadow.Y); - instance.SetParameter("shadow_color", dropShadow.Color); - // TODO: offset - break; - case DreamFilterLayer layer: - break; - case DreamFilterMotionBlur motionBlur: - break; - case DreamFilterOutline outline: - instance.SetParameter("size", outline.Size); - instance.SetParameter("color", outline.Color); - instance.SetParameter("flags", outline.Flags); - break; - case DreamFilterRadialBlur radialBlur: - break; - case DreamFilterRays rays: - break; - case DreamFilterRipple ripple: - break; - case DreamFilterWave wave: - break; - case DreamFilterGreyscale greyscale: - break; - } - } + public ShaderInstance GetFilterShader(DreamFilter filter, Dictionary renderSourceLookup) { + if (!_filterShaders.TryGetValue(filter, out var instance)) { + var protoManager = IoCManager.Resolve(); - // Texture parameters need reset because different render targets can be used each frame + instance = protoManager.Index(filter.FilterType).InstanceUnique(); switch (filter) { case DreamFilterAlpha alpha: - if (!string.IsNullOrEmpty(alpha.RenderSource) && renderSourceLookup.TryGetValue(alpha.RenderSource, out var renderSourceTexture)) - instance.SetParameter("mask_texture", renderSourceTexture.Texture); - else if (alpha.Icon != 0) { - _dreamResourceManager.LoadResourceAsync(alpha.Icon, rsc => { - instance.SetParameter("mask_texture", rsc.Texture); - }); - } else { - instance.SetParameter("mask_texture", Texture.Transparent); - } - + instance.SetParameter("x",alpha.X); + instance.SetParameter("y",alpha.Y); + instance.SetParameter("flags",alpha.Flags); + break; + case DreamFilterAngularBlur angularBlur: + break; + case DreamFilterBloom bloom: + break; + case DreamFilterBlur blur: + instance.SetParameter("size", blur.Size); + break; + case DreamFilterColor color: { + //Since SWSL doesn't support 4x5 matrices, we need to get a bit silly. + instance.SetParameter("colorMatrix", color.Color.GetMatrix4()); + instance.SetParameter("offsetVector", color.Color.GetOffsetVector()); + //TODO: Support the alternative colour mappings. break; + } case DreamFilterDisplace displace: - if (!string.IsNullOrEmpty(displace.RenderSource) && renderSourceLookup.TryGetValue(displace.RenderSource, out renderSourceTexture)) { - instance.SetParameter("displacement_map", renderSourceTexture.Texture); - } else if (displace.Icon != 0) { - _dreamResourceManager.LoadResourceAsync(displace.Icon, rsc => { - instance.SetParameter("displacement_map", rsc.Texture); - }); - } else { - instance.SetParameter("displacement_map", Texture.Transparent); - } - + instance.SetParameter("size", displace.Size); + instance.SetParameter("x", displace.X); + instance.SetParameter("y", displace.Y); + break; + case DreamFilterDropShadow dropShadow: + instance.SetParameter("size", dropShadow.Size); + instance.SetParameter("x", dropShadow.X); + instance.SetParameter("y", dropShadow.Y); + instance.SetParameter("shadow_color", dropShadow.Color); + // TODO: offset + break; + case DreamFilterLayer layer: + break; + case DreamFilterMotionBlur motionBlur: + break; + case DreamFilterOutline outline: + instance.SetParameter("size", outline.Size); + instance.SetParameter("color", outline.Color); + instance.SetParameter("flags", outline.Flags); + break; + case DreamFilterRadialBlur radialBlur: + break; + case DreamFilterRays rays: + break; + case DreamFilterRipple ripple: + break; + case DreamFilterWave wave: + break; + case DreamFilterGreyscale greyscale: break; } + } - filter.Used = true; - _filterShaders[filter] = instance; - return instance; + // Texture parameters need reset because different render targets can be used each frame + switch (filter) { + case DreamFilterAlpha alpha: + if (!string.IsNullOrEmpty(alpha.RenderSource) && renderSourceLookup.TryGetValue(alpha.RenderSource, out var renderSourceTexture)) + instance.SetParameter("mask_texture", renderSourceTexture.Texture); + else if (alpha.Icon != 0) { + _dreamResourceManager.LoadResourceAsync(alpha.Icon, rsc => { + instance.SetParameter("mask_texture", rsc.Texture); + }); + } else { + instance.SetParameter("mask_texture", Texture.Transparent); + } + + break; + case DreamFilterDisplace displace: + if (!string.IsNullOrEmpty(displace.RenderSource) && renderSourceLookup.TryGetValue(displace.RenderSource, out renderSourceTexture)) { + instance.SetParameter("displacement_map", renderSourceTexture.Texture); + } else if (displace.Icon != 0) { + _dreamResourceManager.LoadResourceAsync(displace.Icon, rsc => { + instance.SetParameter("displacement_map", rsc.Texture); + }); + } else { + instance.SetParameter("displacement_map", Texture.Transparent); + } + + break; } + + filter.Used = true; + _filterShaders[filter] = instance; + return instance; } } - diff --git a/OpenDreamClient/Rendering/ClientImagesSystem.cs b/OpenDreamClient/Rendering/ClientImagesSystem.cs index 7f3c89a357..a7be99237e 100644 --- a/OpenDreamClient/Rendering/ClientImagesSystem.cs +++ b/OpenDreamClient/Rendering/ClientImagesSystem.cs @@ -1,14 +1,18 @@ using System.Diagnostics.CodeAnalysis; using OpenDreamShared.Rendering; +using Robust.Shared.Timing; using Vector3 = Robust.Shared.Maths.Vector3; namespace OpenDreamClient.Rendering; -sealed class ClientImagesSystem : SharedClientImagesSystem { - private readonly Dictionary> TurfClientImages = new(); - private readonly Dictionary> AMClientImages = new(); + +internal sealed class ClientImagesSystem : SharedClientImagesSystem { + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!; + + private readonly Dictionary> _turfClientImages = new(); + private readonly Dictionary> _amClientImages = new(); private readonly Dictionary _idToIcon = new(); - [Dependency] private readonly ClientAppearanceSystem _clientAppearanceSystem = default!; - [Dependency] private IEntityManager _entityManager = default!; public override void Initialize() { SubscribeNetworkEvent(OnAddClientImage); @@ -16,8 +20,8 @@ public override void Initialize() { } public override void Shutdown() { - TurfClientImages.Clear(); - AMClientImages.Clear(); + _turfClientImages.Clear(); + _amClientImages.Clear(); _idToIcon.Clear(); } @@ -25,10 +29,10 @@ public bool TryGetClientImages(EntityUid entity, Vector3? tileCoords, [NotNullWh result = null; List? resultIDs; if(entity == EntityUid.Invalid && tileCoords is not null) { - if(!TurfClientImages.TryGetValue(tileCoords.Value, out resultIDs)) + if(!_turfClientImages.TryGetValue(tileCoords.Value, out resultIDs)) return false; } else { - if(!AMClientImages.TryGetValue(entity, out resultIDs)) + if(!_amClientImages.TryGetValue(entity, out resultIDs)) return false; } result = new List(); @@ -41,23 +45,23 @@ public bool TryGetClientImages(EntityUid entity, Vector3? tileCoords, [NotNullWh private void OnAddClientImage(AddClientImageEvent e) { EntityUid ent = _entityManager.GetEntity(e.AttachedEntity); if(ent == EntityUid.Invalid) { - if(!TurfClientImages.TryGetValue(e.TurfCoords, out var iconList)) + if(!_turfClientImages.TryGetValue(e.TurfCoords, out var iconList)) iconList = new List(); if(!_idToIcon.ContainsKey(e.ImageAppearance)){ - DreamIcon icon = new DreamIcon(e.ImageAppearance); + DreamIcon icon = new DreamIcon(_gameTiming, _appearanceSystem, e.ImageAppearance); _idToIcon[e.ImageAppearance] = icon; } iconList.Add(e.ImageAppearance); - TurfClientImages[e.TurfCoords] = iconList; + _turfClientImages[e.TurfCoords] = iconList; } else { - if(!AMClientImages.TryGetValue(ent, out var iconList)) + if(!_amClientImages.TryGetValue(ent, out var iconList)) iconList = new List(); if(!_idToIcon.ContainsKey(e.ImageAppearance)){ - DreamIcon icon = new DreamIcon(e.ImageAppearance); + DreamIcon icon = new DreamIcon(_gameTiming, _appearanceSystem, e.ImageAppearance); _idToIcon[e.ImageAppearance] = icon; } iconList.Add(e.ImageAppearance); - AMClientImages[ent] = iconList; + _amClientImages[ent] = iconList; } } @@ -65,16 +69,16 @@ private void OnAddClientImage(AddClientImageEvent e) { private void OnRemoveClientImage(RemoveClientImageEvent e) { EntityUid ent = _entityManager.GetEntity(e.AttachedEntity); if(ent == EntityUid.Invalid) { - if(!TurfClientImages.TryGetValue(e.TurfCoords, out var iconList)) + if(!_turfClientImages.TryGetValue(e.TurfCoords, out var iconList)) return; iconList.Remove(e.ImageAppearance); - TurfClientImages[e.TurfCoords] = iconList; + _turfClientImages[e.TurfCoords] = iconList; _idToIcon.Remove(e.ImageAppearance); } else { - if(!AMClientImages.TryGetValue(ent, out var iconList)) + if(!_amClientImages.TryGetValue(ent, out var iconList)) return; iconList.Remove(e.ImageAppearance); - AMClientImages[ent] = iconList; + _amClientImages[ent] = iconList; _idToIcon.Remove(e.ImageAppearance); } } diff --git a/OpenDreamClient/Rendering/DMISpriteComponent.cs b/OpenDreamClient/Rendering/DMISpriteComponent.cs index fede2a08c7..f2ea0f43b6 100644 --- a/OpenDreamClient/Rendering/DMISpriteComponent.cs +++ b/OpenDreamClient/Rendering/DMISpriteComponent.cs @@ -5,16 +5,10 @@ namespace OpenDreamClient.Rendering; [RegisterComponent] internal sealed partial class DMISpriteComponent : SharedDMISpriteComponent { - [ViewVariables] public DreamIcon Icon { get; set; } = new DreamIcon(); + [ViewVariables] public DreamIcon Icon { get; set; } [ViewVariables] public ScreenLocation? ScreenLocation { get; set; } [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemMan = default!; - private EntityLookupSystem? _lookupSystem; - - public DMISpriteComponent() { - Icon.SizeChanged += OnIconSizeChanged; - } public bool IsVisible(bool checkWorld = true, int seeInvis = 0) { if (Icon.Appearance?.Invisibility > seeInvis) return false; @@ -31,10 +25,4 @@ public bool IsVisible(bool checkWorld = true, int seeInvis = 0) { return true; } - - private void OnIconSizeChanged() { - _entityManager.TryGetComponent(Owner, out var transform); - _lookupSystem ??= _entitySystemMan.GetEntitySystem(); - _lookupSystem?.FindAndAddToEntityTree(Owner, xform: transform); - } } diff --git a/OpenDreamClient/Rendering/DMISpriteSystem.cs b/OpenDreamClient/Rendering/DMISpriteSystem.cs index f78e51bba5..232855afd4 100644 --- a/OpenDreamClient/Rendering/DMISpriteSystem.cs +++ b/OpenDreamClient/Rendering/DMISpriteSystem.cs @@ -1,13 +1,30 @@ using OpenDreamShared.Rendering; using Robust.Shared.GameStates; +using Robust.Shared.Timing; namespace OpenDreamClient.Rendering; public sealed class DMISpriteSystem : EntitySystem { + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; + [Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!; + public override void Initialize() { + SubscribeLocalEvent(HandleComponentAdd); SubscribeLocalEvent(HandleComponentState); } + private void OnIconSizeChanged(EntityUid uid) { + _entityManager.TryGetComponent(uid, out var transform); + _lookupSystem.FindAndAddToEntityTree(uid, xform: transform); + } + + private void HandleComponentAdd(EntityUid uid, DMISpriteComponent component, ref ComponentAdd args) { + component.Icon = new DreamIcon(_gameTiming, _appearanceSystem); + component.Icon.SizeChanged += () => OnIconSizeChanged(uid); + } + private static void HandleComponentState(EntityUid uid, DMISpriteComponent component, ref ComponentHandleState args) { SharedDMISpriteComponent.DMISpriteComponentState? state = (SharedDMISpriteComponent.DMISpriteComponentState?)args.Current; if (state == null) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index 3188f8c721..9918081c54 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -3,216 +3,206 @@ using OpenDreamShared.Dream; using OpenDreamShared.Resources; using Robust.Client.Graphics; +using Robust.Shared.Timing; -namespace OpenDreamClient.Rendering { - internal sealed class DreamIcon { - public delegate void SizeChangedEventHandler(); - - public List Overlays { get; } = new(); - public List Underlays { get; } = new(); - public event SizeChangedEventHandler? SizeChanged; - private ClientAppearanceSystem? appearanceSystem = null; - public DMIResource? DMI { - get => _dmi; - private set { - _dmi = value; - CheckSizeChange(); - } - } - private DMIResource? _dmi; +namespace OpenDreamClient.Rendering; - public int AnimationFrame { - get { - UpdateAnimation(); - return _animationFrame; - } - } +internal sealed class DreamIcon(IGameTiming gameTiming, ClientAppearanceSystem appearanceSystem) { + public delegate void SizeChangedEventHandler(); - [ViewVariables] - public IconAppearance? Appearance { - get => CalculateAnimatedAppearance(); - private set { - _appearance = value; - UpdateIcon(); - } + public List Overlays { get; } = new(); + public List Underlays { get; } = new(); + public event SizeChangedEventHandler? SizeChanged; + public DMIResource? DMI { + get => _dmi; + private set { + _dmi = value; + CheckSizeChange(); } - private IconAppearance? _appearance; + } + private DMIResource? _dmi; - public AtlasTexture? CurrentFrame { - get => (Appearance == null || DMI == null) ? null : DMI.GetState(Appearance.IconState)?.GetFrames(Appearance.Direction)[AnimationFrame]; + public int AnimationFrame { + get { + UpdateAnimation(); + return _animationFrame; } + } - private int _animationFrame; - private DateTime _animationFrameTime = DateTime.Now; - private AppearanceAnimation? _appearanceAnimation = null; - private Box2? _cachedAABB = null; - - public DreamIcon() { } - - public DreamIcon(int appearanceId, AtomDirection? parentDir = null) { - SetAppearance(appearanceId, parentDir); + [ViewVariables] + public IconAppearance? Appearance { + get => CalculateAnimatedAppearance(); + private set { + _appearance = value; + UpdateIcon(); } + } + private IconAppearance? _appearance; - public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { - // End any animations that are currently happening - // Note that this isn't faithful to the original behavior - EndAppearanceAnimation(); + public AtlasTexture? CurrentFrame => (Appearance == null || DMI == null) + ? null + : DMI.GetState(Appearance.IconState)?.GetFrames(Appearance.Direction)[AnimationFrame]; - if (appearanceId == null) { - Appearance = null; - return; - } + private int _animationFrame; + private TimeSpan _animationFrameTime = gameTiming.CurTime; + private AppearanceAnimation? _appearanceAnimation; + private Box2? _cachedAABB; - //for some reason, doing this as a dependency doesn't initialise it in time so a null ref happens - appearanceSystem ??= EntitySystem.Get(); + public DreamIcon(IGameTiming gameTiming, ClientAppearanceSystem appearanceSystem, int appearanceId, + AtomDirection? parentDir = null) : this(gameTiming, appearanceSystem) { + SetAppearance(appearanceId, parentDir); + } - appearanceSystem.LoadAppearance(appearanceId.Value, appearance => { - if (parentDir != null && appearance.InheritsDirection) { - appearance = new IconAppearance(appearance) { - Direction = parentDir.Value - }; - } + public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { + // End any animations that are currently happening + // Note that this isn't faithful to the original behavior + EndAppearanceAnimation(); - Appearance = appearance; - }); + if (appearanceId == null) { + Appearance = null; + return; } - public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan duration) { - _appearance = CalculateAnimatedAppearance(); //Animation starts from the current animated appearance - _appearanceAnimation = new AppearanceAnimation(DateTime.Now, duration, endingAppearance); - } + appearanceSystem.LoadAppearance(appearanceId.Value, appearance => { + if (parentDir != null && appearance.InheritsDirection) { + appearance = new IconAppearance(appearance) { + Direction = parentDir.Value + }; + } - public void EndAppearanceAnimation() { - if (_appearanceAnimation != null) - _appearance = _appearanceAnimation.Value.EndAppearance; + Appearance = appearance; + }); + } - _appearanceAnimation = null; - } + public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan duration) { + _appearance = CalculateAnimatedAppearance(); //Animation starts from the current animated appearance + _appearanceAnimation = new AppearanceAnimation(DateTime.Now, duration, endingAppearance); + } - public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { - if (DMI != null && Appearance != null) { - Vector2 size = DMI.IconSize / (float)EyeManager.PixelsPerMeter; - Vector2 pixelOffset = Appearance.PixelOffset / (float)EyeManager.PixelsPerMeter; + public void EndAppearanceAnimation() { + if (_appearanceAnimation != null) + _appearance = _appearanceAnimation.Value.EndAppearance; - worldPos += pixelOffset; + _appearanceAnimation = null; + } - Box2 thisAABB = Box2.CenteredAround(worldPos, size); - aabb = aabb?.Union(thisAABB) ?? thisAABB; - } + public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { + if (DMI != null && Appearance != null) { + Vector2 size = DMI.IconSize / (float)EyeManager.PixelsPerMeter; + Vector2 pixelOffset = Appearance.PixelOffset / (float)EyeManager.PixelsPerMeter; - foreach (DreamIcon underlay in Underlays) { - underlay.GetWorldAABB(worldPos, ref aabb); - } + worldPos += pixelOffset; - foreach (DreamIcon overlay in Overlays) { - overlay.GetWorldAABB(worldPos, ref aabb); - } + Box2 thisAABB = Box2.CenteredAround(worldPos, size); + aabb = aabb?.Union(thisAABB) ?? thisAABB; } - private void UpdateAnimation() { - if(DMI == null || Appearance == null) - return; - DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(Appearance.IconState); - if(dmiState == null) - return; - DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(Appearance.Direction); - - if (_animationFrame == frames.Length - 1 && !dmiState.Loop) return; - - double elapsedTime = DateTime.Now.Subtract(_animationFrameTime).TotalMilliseconds; - while (elapsedTime >= frames[_animationFrame].Delay) { - elapsedTime -= frames[_animationFrame].Delay; - _animationFrameTime = _animationFrameTime.AddMilliseconds(frames[_animationFrame].Delay); - _animationFrame++; + foreach (DreamIcon underlay in Underlays) { + underlay.GetWorldAABB(worldPos, ref aabb); + } - if (_animationFrame >= frames.Length) _animationFrame -= frames.Length; - } + foreach (DreamIcon overlay in Overlays) { + overlay.GetWorldAABB(worldPos, ref aabb); } + } - private IconAppearance? CalculateAnimatedAppearance() { - if (_appearanceAnimation == null || _appearance == null) - return _appearance; + private void UpdateAnimation() { + if(DMI == null || Appearance == null) + return; + DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(Appearance.IconState); + if(dmiState == null) + return; + DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(Appearance.Direction); - AppearanceAnimation animation = _appearanceAnimation.Value; - IconAppearance appearance = new IconAppearance(_appearance); - float factor = Math.Clamp((float)(DateTime.Now - animation.Start).Ticks / animation.Duration.Ticks, 0.0f, 1.0f); - IconAppearance endAppearance = animation.EndAppearance; + if (_animationFrame == frames.Length - 1 && !dmiState.Loop) return; - if (endAppearance.PixelOffset != _appearance.PixelOffset) { - Vector2 startingOffset = appearance.PixelOffset; - Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset, factor); + TimeSpan elapsedTime = gameTiming.CurTime.Subtract(_animationFrameTime); + while (elapsedTime >= frames[_animationFrame].Delay) { + elapsedTime -= frames[_animationFrame].Delay; + _animationFrameTime += frames[_animationFrame].Delay; + _animationFrame++; - appearance.PixelOffset = (Vector2i)newPixelOffset; - } + if (_animationFrame >= frames.Length) _animationFrame -= frames.Length; + } + } - if (endAppearance.Direction != _appearance.Direction) { - appearance.Direction = endAppearance.Direction; - } + private IconAppearance? CalculateAnimatedAppearance() { + if (_appearanceAnimation == null || _appearance == null) + return _appearance; - // TODO: Other animatable properties + AppearanceAnimation animation = _appearanceAnimation.Value; + IconAppearance appearance = new IconAppearance(_appearance); + float factor = Math.Clamp((float)(DateTime.Now - animation.Start).Ticks / animation.Duration.Ticks, 0.0f, 1.0f); + IconAppearance endAppearance = animation.EndAppearance; - if (factor >= 1f) { - EndAppearanceAnimation(); - } + if (endAppearance.PixelOffset != _appearance.PixelOffset) { + Vector2 startingOffset = appearance.PixelOffset; + Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset, factor); - return appearance; + appearance.PixelOffset = (Vector2i)newPixelOffset; } - private void UpdateIcon() { - if (Appearance == null) { - DMI = null; - return; - } + if (endAppearance.Direction != _appearance.Direction) { + appearance.Direction = endAppearance.Direction; + } - if (Appearance.Icon == null) { - DMI = null; - } else { - IoCManager.Resolve().LoadResourceAsync(Appearance.Icon.Value, dmi => { - if (dmi.Id != Appearance.Icon) return; //Icon changed while resource was loading + // TODO: Other animatable properties - DMI = dmi; - _animationFrame = 0; - _animationFrameTime = DateTime.Now; - }); - } + if (factor >= 1f) { + EndAppearanceAnimation(); + } - Overlays.Clear(); - foreach (int overlayId in Appearance.Overlays) { - DreamIcon overlay = new DreamIcon(overlayId, Appearance.Direction); - overlay.SizeChanged += CheckSizeChange; + return appearance; + } - Overlays.Add(overlay); - } + private void UpdateIcon() { + if (Appearance == null) { + DMI = null; + return; + } - Underlays.Clear(); - foreach (int underlayId in Appearance.Underlays) { - DreamIcon underlay = new DreamIcon(underlayId, Appearance.Direction); - underlay.SizeChanged += CheckSizeChange; + if (Appearance.Icon == null) { + DMI = null; + } else { + IoCManager.Resolve().LoadResourceAsync(Appearance.Icon.Value, dmi => { + if (dmi.Id != Appearance.Icon) return; //Icon changed while resource was loading - Underlays.Add(underlay); - } + DMI = dmi; + _animationFrame = 0; + _animationFrameTime = gameTiming.CurTime; + }); } - private void CheckSizeChange() { - Box2? aabb = null; - GetWorldAABB(Vector2.Zero, ref aabb); + Overlays.Clear(); + foreach (int overlayId in Appearance.Overlays) { + DreamIcon overlay = new DreamIcon(gameTiming, appearanceSystem, overlayId, Appearance.Direction); + overlay.SizeChanged += CheckSizeChange; - if (aabb != _cachedAABB) { - _cachedAABB = aabb; - SizeChanged?.Invoke(); - } + Overlays.Add(overlay); } - private struct AppearanceAnimation { - public readonly DateTime Start; - public readonly TimeSpan Duration; - public readonly IconAppearance EndAppearance; + Underlays.Clear(); + foreach (int underlayId in Appearance.Underlays) { + DreamIcon underlay = new DreamIcon(gameTiming, appearanceSystem, underlayId, Appearance.Direction); + underlay.SizeChanged += CheckSizeChange; - public AppearanceAnimation(DateTime start, TimeSpan duration, IconAppearance endAppearance) { - Start = start; - Duration = duration; - EndAppearance = endAppearance; - } + Underlays.Add(underlay); } } + + private void CheckSizeChange() { + Box2? aabb = null; + GetWorldAABB(Vector2.Zero, ref aabb); + + if (aabb != _cachedAABB) { + _cachedAABB = aabb; + SizeChanged?.Invoke(); + } + } + + private struct AppearanceAnimation(DateTime start, TimeSpan duration, IconAppearance endAppearance) { + public readonly DateTime Start = start; + public readonly TimeSpan Duration = duration; + public readonly IconAppearance EndAppearance = endAppearance; + } } diff --git a/OpenDreamShared/Resources/DMIParser.cs b/OpenDreamShared/Resources/DMIParser.cs index 6e7d4b7e18..50bc2001f2 100644 --- a/OpenDreamShared/Resources/DMIParser.cs +++ b/OpenDreamShared/Resources/DMIParser.cs @@ -8,452 +8,456 @@ using OpenDreamShared.Dream; using System.Globalization; -namespace OpenDreamShared.Resources { - public static class DMIParser { - public static readonly AtomDirection[] DMIFrameDirections = { - AtomDirection.South, - AtomDirection.North, - AtomDirection.East, - AtomDirection.West, - AtomDirection.Southeast, - AtomDirection.Southwest, - AtomDirection.Northeast, - AtomDirection.Northwest - }; - - private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA }; - - public sealed class ParsedDMIDescription { - public int Width, Height; - public Dictionary States; - - /// - /// Gets the requested state, or the default if it doesn't exist - /// - /// The default state could also not exist - /// The requested state's name - /// The requested state, default state, or null - public ParsedDMIState? GetStateOrDefault(string? stateName) { - if (string.IsNullOrEmpty(stateName) || !States.TryGetValue(stateName, out var state)) { - States.TryGetValue(string.Empty, out state); - } +namespace OpenDreamShared.Resources; + +public static class DMIParser { + public static readonly AtomDirection[] DMIFrameDirections = { + AtomDirection.South, + AtomDirection.North, + AtomDirection.East, + AtomDirection.West, + AtomDirection.Southeast, + AtomDirection.Southwest, + AtomDirection.Northeast, + AtomDirection.Northwest + }; + + private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA }; + + public sealed class ParsedDMIDescription { + public int Width, Height; + public Dictionary States; - return state; + /// + /// Gets the requested state, or the default if it doesn't exist + /// + /// The default state could also not exist + /// The requested state's name + /// The requested state, default state, or null + public ParsedDMIState? GetStateOrDefault(string? stateName) { + if (string.IsNullOrEmpty(stateName) || !States.TryGetValue(stateName, out var state)) { + States.TryGetValue(string.Empty, out state); } - /// - /// Construct a string describing this DMI description
- /// In the same format as the text found in .dmi files - ///
- /// This ParsedDMIDescription represented as text - public string ExportAsText() { - StringBuilder text = new(); - - text.AppendLine("# BEGIN DMI"); - - // This could either end up compressed or decompressed depending on how large this text ends up being. - // So go with version 3.0, BYOND doesn't seem to care either way - text.AppendLine("version = 3.0"); - text.Append("\twidth = "); - text.Append(Width); - text.AppendLine(); - text.Append("\theight = "); - text.Append(Height); - text.AppendLine(); + return state; + } - foreach (var state in States.Values) { - state.ExportAsText(text); - } + /// + /// Construct a string describing this DMI description
+ /// In the same format as the text found in .dmi files + ///
+ /// This ParsedDMIDescription represented as text + public string ExportAsText() { + StringBuilder text = new(); + + text.AppendLine("# BEGIN DMI"); + + // This could either end up compressed or decompressed depending on how large this text ends up being. + // So go with version 3.0, BYOND doesn't seem to care either way + text.AppendLine("version = 3.0"); + text.Append("\twidth = "); + text.Append(Width); + text.AppendLine(); + text.Append("\theight = "); + text.Append(Height); + text.AppendLine(); + + foreach (var state in States.Values) { + state.ExportAsText(text); + } - text.Append("# END DMI"); + text.Append("# END DMI"); - return text.ToString(); - } + return text.ToString(); } + } - public sealed class ParsedDMIState { - public string Name; - public bool Loop = true; - public bool Rewind = false; + public sealed class ParsedDMIState { + public string Name; + public bool Loop = true; + public bool Rewind = false; - // TODO: This can only contain either 1, 4, or 8 directions. Enforcing this could simplify some things. - public readonly Dictionary Directions = new(); + // TODO: This can only contain either 1, 4, or 8 directions. Enforcing this could simplify some things. + public readonly Dictionary Directions = new(); - /// - /// The amount of animation frames this state has - /// - public int FrameCount { - get { - if (Directions.Count == 0) - return 0; + /// + /// The amount of animation frames this state has + /// + public int FrameCount { + get { + if (Directions.Count == 0) + return 0; - return Directions.Values.First().Length; - } + return Directions.Values.First().Length; } + } - public ParsedDMIFrame[] GetFrames(AtomDirection direction = AtomDirection.South) { - // Find another direction to use if this one doesn't exist - if (!Directions.ContainsKey(direction)) { - // The diagonal directions attempt to use east/west - if (direction is AtomDirection.Northeast or AtomDirection.Southeast) - direction = AtomDirection.East; - else if (direction is AtomDirection.Northwest or AtomDirection.Southwest) - direction = AtomDirection.West; - - // Use the south direction if the above still isn't valid - if (!Directions.ContainsKey(direction)) - direction = AtomDirection.South; - } - - return Directions[direction]; + public ParsedDMIFrame[] GetFrames(AtomDirection direction = AtomDirection.South) { + // Find another direction to use if this one doesn't exist + if (!Directions.ContainsKey(direction)) { + // The diagonal directions attempt to use east/west + if (direction is AtomDirection.Northeast or AtomDirection.Southeast) + direction = AtomDirection.East; + else if (direction is AtomDirection.Northwest or AtomDirection.Southwest) + direction = AtomDirection.West; + + // Use the south direction if the above still isn't valid + if (!Directions.ContainsKey(direction)) + direction = AtomDirection.South; } - public void ExportAsText(StringBuilder text) { - text.Append("state = \""); - text.Append(Name); - text.AppendLine("\""); - - text.Append("\tdirs = "); - text.Append(GetExportedDirectionCount(Directions)); - text.AppendLine(); + return Directions[direction]; + } - text.Append("\tframes = "); - text.Append(FrameCount); - text.AppendLine(); + public void ExportAsText(StringBuilder text) { + text.Append("state = \""); + text.Append(Name); + text.AppendLine("\""); - if (Directions.Count > 0) { - text.Append("\tdelay = "); - var frames = Directions.Values.First(); // Delays should be the same in each direction - for (int i = 0; i < frames.Length; i++) { - var delay = frames[i].Delay / 100; // Convert back to deciseconds + text.Append("\tdirs = "); + text.Append(GetExportedDirectionCount(Directions)); + text.AppendLine(); - text.Append(delay.ToString(CultureInfo.InvariantCulture)); - if (i != frames.Length - 1) - text.Append(','); - } - text.AppendLine(); - } + text.Append("\tframes = "); + text.Append(FrameCount); + text.AppendLine(); - if (!Loop) { - text.AppendLine("\tloop = 0"); - } + if (Directions.Count > 0) { + text.Append("\tdelay = "); + var frames = Directions.Values.First(); // Delays should be the same in each direction + for (int i = 0; i < frames.Length; i++) { + var delay = frames[i].Delay.TotalMilliseconds / 100; // Convert back to deciseconds - if (Rewind) { - text.AppendLine("\trewind = 1"); + text.Append(delay.ToString(CultureInfo.InvariantCulture)); + if (i != frames.Length - 1) + text.Append(','); } + text.AppendLine(); } - /// - /// Get this state's frames - /// - /// Which direction to get. Every direction if null. - /// Which frame to get. Every frame if null. - /// If dir isn't null, return the frames as facing south - /// Invalid dir/frame args will give empty arrays - /// A dictionary containing the specified frames for each specified direction - public Dictionary GetFrames(AtomDirection? dir = null, int? frame = null, bool asSouth = false) { - Dictionary directions; - if (dir == null) { // Get every direction - directions = new(Directions); - } else { - directions = new(1); - - if (!Directions.TryGetValue(dir.Value, out var frames)) - frames = Array.Empty(); - - directions.Add(asSouth ? AtomDirection.South : dir.Value, frames); - } - - if (frame != null) { // Only get a specified frame - foreach (var direction in directions) { - if (direction.Value.Length > frame.Value) { - directions[direction.Key] = new[] { direction.Value[frame.Value] }; - } else { - // Frame doesn't exist - directions[direction.Key] = Array.Empty(); - } - } - } - - return directions; + if (!Loop) { + text.AppendLine("\tloop = 0"); } - } - public sealed class ParsedDMIFrame { - public int X, Y; - public float Delay; + if (Rewind) { + text.AppendLine("\trewind = 1"); + } } /// - /// The total directions present in an exported DMI.
- /// An icon state in a DMI must contain either 1, 4, or 8 directions. + /// Get this state's frames ///
- public static int GetExportedDirectionCount(Dictionary directions) { - // If we have any of these directions then we export 8 directions - if (directions.ContainsKey(AtomDirection.Northeast) || directions.ContainsKey(AtomDirection.Southeast) || - directions.ContainsKey(AtomDirection.Southwest) || directions.ContainsKey(AtomDirection.Northwest)) { - return 8; + /// Which direction to get. Every direction if null. + /// Which frame to get. Every frame if null. + /// If dir isn't null, return the frames as facing south + /// Invalid dir/frame args will give empty arrays + /// A dictionary containing the specified frames for each specified direction + public Dictionary GetFrames(AtomDirection? dir = null, int? frame = null, bool asSouth = false) { + Dictionary directions; + if (dir == null) { // Get every direction + directions = new(Directions); + } else { + directions = new(1); + + if (!Directions.TryGetValue(dir.Value, out var frames)) + frames = Array.Empty(); + + directions.Add(asSouth ? AtomDirection.South : dir.Value, frames); } - // Any of these (without the above) means 4 directions - if (directions.ContainsKey(AtomDirection.North) || directions.ContainsKey(AtomDirection.East) || - directions.ContainsKey(AtomDirection.West)) { - return 4; + if (frame != null) { // Only get a specified frame + foreach (var direction in directions) { + if (direction.Value.Length > frame.Value) { + directions[direction.Key] = new[] { direction.Value[frame.Value] }; + } else { + // Frame doesn't exist + directions[direction.Key] = Array.Empty(); + } + } } - // Otherwise, 1 direction (just south) - return 1; + return directions; } + } - public static ParsedDMIDescription ParseDMI(Stream stream) { - if (!VerifyPNG(stream)) throw new Exception("Provided stream was not a valid PNG"); - - BinaryReader reader = new BinaryReader(stream); - Vector2u? imageSize = null; + public sealed class ParsedDMIFrame { + public int X, Y; + public TimeSpan Delay; + } - while (stream.Position < stream.Length) { - long chunkDataPosition = stream.Position; - uint chunkLength = ReadBigEndianUint32(reader); - string chunkType = new string(reader.ReadChars(4)); + /// + /// The total directions present in an exported DMI.
+ /// An icon state in a DMI must contain either 1, 4, or 8 directions. + ///
+ public static int GetExportedDirectionCount(Dictionary directions) { + // If we have any of these directions then we export 8 directions + if (directions.ContainsKey(AtomDirection.Northeast) || directions.ContainsKey(AtomDirection.Southeast) || + directions.ContainsKey(AtomDirection.Southwest) || directions.ContainsKey(AtomDirection.Northwest)) { + return 8; + } - switch (chunkType) { - case "IHDR": //Image header, contains the image size - imageSize = new Vector2u(ReadBigEndianUint32(reader), ReadBigEndianUint32(reader)); - stream.Seek(chunkLength - 4, SeekOrigin.Current); //Skip the rest of the chunk - break; - case "zTXt": //Compressed text, likely contains our DMI description - case "tEXt": //Uncompressed text. Not typical, but also works. - if (imageSize == null) throw new Exception("The PNG did not contain an IHDR chunk"); + // Any of these (without the above) means 4 directions + if (directions.ContainsKey(AtomDirection.North) || directions.ContainsKey(AtomDirection.East) || + directions.ContainsKey(AtomDirection.West)) { + return 4; + } - StringBuilder keyword = new StringBuilder(); - while (reader.PeekChar() != 0 && keyword.Length < 79) { - keyword.Append(reader.ReadChar()); - } + // Otherwise, 1 direction (just south) + return 1; + } - stream.Seek(1, SeekOrigin.Current); //Skip over null-terminator - if (chunkType == "zTXt") - stream.Seek(1, SeekOrigin.Current); //Skip over compression type + public static ParsedDMIDescription ParseDMI(Stream stream) { + if (!VerifyPNG(stream)) throw new Exception("Provided stream was not a valid PNG"); + + BinaryReader reader = new BinaryReader(stream); + Vector2u? imageSize = null; + + while (stream.Position < stream.Length) { + long chunkDataPosition = stream.Position; + uint chunkLength = ReadBigEndianUint32(reader); + string chunkType = new string(reader.ReadChars(4)); + + switch (chunkType) { + case "IHDR": //Image header, contains the image size + imageSize = new Vector2u(ReadBigEndianUint32(reader), ReadBigEndianUint32(reader)); + stream.Seek(chunkLength - 4, SeekOrigin.Current); //Skip the rest of the chunk + break; + case "zTXt": //Compressed text, likely contains our DMI description + case "tEXt": //Uncompressed text. Not typical, but also works. + if (imageSize == null) throw new Exception("The PNG did not contain an IHDR chunk"); + + StringBuilder keyword = new StringBuilder(); + while (reader.PeekChar() != 0 && keyword.Length < 79) { + keyword.Append(reader.ReadChar()); + } - if (keyword.ToString() == "Description") { - byte[] uncompressedData; + stream.Seek(1, SeekOrigin.Current); //Skip over null-terminator + if (chunkType == "zTXt") + stream.Seek(1, SeekOrigin.Current); //Skip over compression type - if (chunkType == "zTXt") { - stream.Seek(2, SeekOrigin.Current); //Skip the first 2 bytes in the zlib format + if (keyword.ToString() == "Description") { + byte[] uncompressedData; - DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress); - MemoryStream uncompressedDataStream = new MemoryStream(); + if (chunkType == "zTXt") { + stream.Seek(2, SeekOrigin.Current); //Skip the first 2 bytes in the zlib format - deflateStream.CopyTo(uncompressedDataStream, (int)chunkLength - keyword.Length - 2); + DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress); + MemoryStream uncompressedDataStream = new MemoryStream(); - uncompressedData = new byte[uncompressedDataStream.Length]; - uncompressedDataStream.Seek(0, SeekOrigin.Begin); - uncompressedDataStream.Read(uncompressedData); - } else { - //The text is not compressed so nothing fancy is required - uncompressedData = reader.ReadBytes((int) chunkLength - keyword.Length - 1); - } + deflateStream.CopyTo(uncompressedDataStream, (int)chunkLength - keyword.Length - 2); - string dmiDescription = Encoding.UTF8.GetString(uncompressedData, 0, uncompressedData.Length); - return ParseDMIDescription(dmiDescription, imageSize.Value.X); + uncompressedData = new byte[uncompressedDataStream.Length]; + uncompressedDataStream.Seek(0, SeekOrigin.Begin); + uncompressedDataStream.Read(uncompressedData); + } else { + //The text is not compressed so nothing fancy is required + uncompressedData = reader.ReadBytes((int) chunkLength - keyword.Length - 1); } - // Wasn't the description chunk we were looking for - stream.Position = chunkDataPosition + chunkLength + 4; - break; - default: //Nothing we care about, skip it - stream.Seek(chunkLength + 4, SeekOrigin.Current); - break; - } - } + string dmiDescription = Encoding.UTF8.GetString(uncompressedData, 0, uncompressedData.Length); + return ParseDMIDescription(dmiDescription, imageSize.Value.X); + } - if (imageSize != null) { - // No DMI description found, but we do have an image header - // So treat this PNG as a single icon frame spanning the whole image - - var desc = new ParsedDMIDescription() { - Width = (int)imageSize.Value.X, - Height = (int)imageSize.Value.Y, - States = new() - }; - - var state = new ParsedDMIState() { - Name = string.Empty - }; - - var frame = new ParsedDMIFrame() { - X = 0, - Y = 0, - Delay = 1 - }; - - state.Directions.Add(AtomDirection.South, new [] { frame }); - desc.States.Add(state.Name, state); - return desc; + // Wasn't the description chunk we were looking for + stream.Position = chunkDataPosition + chunkLength + 4; + break; + default: //Nothing we care about, skip it + stream.Seek(chunkLength + 4, SeekOrigin.Current); + break; } + } - throw new Exception("PNG is missing an image header"); + if (imageSize != null) { + // No DMI description found, but we do have an image header + // So treat this PNG as a single icon frame spanning the whole image + + var desc = new ParsedDMIDescription() { + Width = (int)imageSize.Value.X, + Height = (int)imageSize.Value.Y, + States = new() + }; + + var state = new ParsedDMIState() { + Name = string.Empty + }; + + var frame = new ParsedDMIFrame() { + X = 0, + Y = 0, + Delay = TimeSpan.FromMilliseconds(100) + }; + + state.Directions.Add(AtomDirection.South, new [] { frame }); + desc.States.Add(state.Name, state); + return desc; } - private static ParsedDMIDescription ParseDMIDescription(string dmiDescription, uint imageWidth) { - ParsedDMIDescription description = new ParsedDMIDescription(); - ParsedDMIState currentState = null; - int currentFrameX = 0; - int currentFrameY = 0; - int currentStateDirectionCount = 1; - int currentStateFrameCount = 1; - float[] currentStateFrameDelays = null; - - description.States = new Dictionary(); - - string[] lines = dmiDescription.Split("\n"); - foreach (string line in lines) { - if (line.StartsWith('#') || string.IsNullOrWhiteSpace(line)) - continue; - - int equalsIndex = line.IndexOf('='); - - if (equalsIndex != -1) { - string key = line.Substring(0, equalsIndex-1).Trim(); - string value = line.Substring(equalsIndex + 1).Trim(); - - switch (key) { - case "version": - // No need to care about this at the moment - break; - case "width": - description.Width = int.Parse(value); - break; - case "height": - description.Height = int.Parse(value); - break; - case "state": - string stateName = ParseString(value); - - if (currentState != null) { - for (int i = 0; i < currentStateDirectionCount; i++) { - ParsedDMIFrame[] frames = new ParsedDMIFrame[currentStateFrameCount]; - AtomDirection direction = DMIFrameDirections[i]; - - currentState.Directions[direction] = frames; - } + throw new Exception("PNG is missing an image header"); + } + + private static ParsedDMIDescription ParseDMIDescription(string dmiDescription, uint imageWidth) { + ParsedDMIDescription description = new ParsedDMIDescription(); + ParsedDMIState currentState = null; + int currentFrameX = 0; + int currentFrameY = 0; + int currentStateDirectionCount = 1; + int currentStateFrameCount = 1; + float[] currentStateFrameDelays = null; + + description.States = new Dictionary(); + + string[] lines = dmiDescription.Split("\n"); + foreach (string line in lines) { + if (line.StartsWith('#') || string.IsNullOrWhiteSpace(line)) + continue; + + int equalsIndex = line.IndexOf('='); + + if (equalsIndex != -1) { + string key = line.Substring(0, equalsIndex-1).Trim(); + string value = line.Substring(equalsIndex + 1).Trim(); - for (int i = 0; i < currentStateFrameCount; i++) { - for (int j = 0; j < currentStateDirectionCount; j++) { - AtomDirection direction = DMIFrameDirections[j]; + switch (key) { + case "version": + // No need to care about this at the moment + break; + case "width": + description.Width = int.Parse(value); + break; + case "height": + description.Height = int.Parse(value); + break; + case "state": + string stateName = ParseString(value); + + if (currentState != null) { + for (int i = 0; i < currentStateDirectionCount; i++) { + ParsedDMIFrame[] frames = new ParsedDMIFrame[currentStateFrameCount]; + AtomDirection direction = DMIFrameDirections[i]; + + currentState.Directions[direction] = frames; + } + + for (int i = 0; i < currentStateFrameCount; i++) { + for (int j = 0; j < currentStateDirectionCount; j++) { + AtomDirection direction = DMIFrameDirections[j]; - ParsedDMIFrame frame = new ParsedDMIFrame(); + ParsedDMIFrame frame = new ParsedDMIFrame(); + float delay = (currentStateFrameDelays != null) + ? currentStateFrameDelays[i] * 100 // Convert from deciseconds to milliseconds + : 100; - frame.X = currentFrameX; - frame.Y = currentFrameY; - frame.Delay = (currentStateFrameDelays != null) ? currentStateFrameDelays[i] : 1; - frame.Delay *= 100; //Convert from deciseconds to milliseconds - currentState.Directions[direction][i] = frame; + frame.X = currentFrameX; + frame.Y = currentFrameY; + frame.Delay = TimeSpan.FromMilliseconds(delay); + currentState.Directions[direction][i] = frame; - currentFrameX += description.Width; - if (currentFrameX >= imageWidth) { - currentFrameY += description.Height; - currentFrameX = 0; - } + currentFrameX += description.Width; + if (currentFrameX >= imageWidth) { + currentFrameY += description.Height; + currentFrameX = 0; } } } + } - currentStateFrameCount = 1; - currentStateFrameDelays = null; - - currentState = new ParsedDMIState(); - currentState.Name = stateName; - if (!description.States.ContainsKey(stateName)) description.States.Add(stateName, currentState); - - break; - case "dirs": - currentStateDirectionCount = int.Parse(value); - break; - case "frames": - currentStateFrameCount = int.Parse(value); - break; - case "delay": - string[] frameDelays = value.Split(","); - - currentStateFrameDelays = new float[frameDelays.Length]; - for (int i = 0; i < frameDelays.Length; i++) { - currentStateFrameDelays[i] = float.Parse(frameDelays[i], CultureInfo.InvariantCulture); - } + currentStateFrameCount = 1; + currentStateFrameDelays = null; - break; - case "loop": - currentState.Loop = (int.Parse(value) == 0); - break; - case "rewind": - currentState.Rewind = (int.Parse(value) == 1); - break; - case "movement": - //TODO - break; - case "hotspot": - //TODO - break; - default: - throw new Exception($"Invalid key \"{key}\" in DMI description"); - } - } else { - throw new Exception($"Invalid line in DMI description: \"{line}\""); + currentState = new ParsedDMIState(); + currentState.Name = stateName; + if (!description.States.ContainsKey(stateName)) description.States.Add(stateName, currentState); + + break; + case "dirs": + currentStateDirectionCount = int.Parse(value); + break; + case "frames": + currentStateFrameCount = int.Parse(value); + break; + case "delay": + string[] frameDelays = value.Split(","); + + currentStateFrameDelays = new float[frameDelays.Length]; + for (int i = 0; i < frameDelays.Length; i++) { + currentStateFrameDelays[i] = float.Parse(frameDelays[i], CultureInfo.InvariantCulture); + } + + break; + case "loop": + currentState.Loop = (int.Parse(value) == 0); + break; + case "rewind": + currentState.Rewind = (int.Parse(value) == 1); + break; + case "movement": + //TODO + break; + case "hotspot": + //TODO + break; + default: + throw new Exception($"Invalid key \"{key}\" in DMI description"); } + } else { + throw new Exception($"Invalid line in DMI description: \"{line}\""); } + } - for (int i = 0; i < currentStateDirectionCount; i++) { - ParsedDMIFrame[] frames = new ParsedDMIFrame[currentStateFrameCount]; - AtomDirection direction = DMIFrameDirections[i]; + for (int i = 0; i < currentStateDirectionCount; i++) { + ParsedDMIFrame[] frames = new ParsedDMIFrame[currentStateFrameCount]; + AtomDirection direction = DMIFrameDirections[i]; - currentState.Directions[direction] = frames; - } + currentState.Directions[direction] = frames; + } - for (int i = 0; i < currentStateFrameCount; i++) { - for (int j = 0; j < currentStateDirectionCount; j++) { - AtomDirection direction = DMIFrameDirections[j]; + for (int i = 0; i < currentStateFrameCount; i++) { + for (int j = 0; j < currentStateDirectionCount; j++) { + AtomDirection direction = DMIFrameDirections[j]; - ParsedDMIFrame frame = new ParsedDMIFrame(); + ParsedDMIFrame frame = new ParsedDMIFrame(); + float delay = (currentStateFrameDelays != null) + ? currentStateFrameDelays[i] * 100 // Convert from deciseconds to milliseconds + : 100; - frame.X = currentFrameX; - frame.Y = currentFrameY; - frame.Delay = (currentStateFrameDelays != null) ? currentStateFrameDelays[i] : 1; - frame.Delay *= 100; //Convert from deciseconds to milliseconds - currentState.Directions[direction][i] = frame; + frame.X = currentFrameX; + frame.Y = currentFrameY; + frame.Delay = TimeSpan.FromMilliseconds(delay); + currentState.Directions[direction][i] = frame; - currentFrameX += description.Width; - if (currentFrameX >= imageWidth) { - currentFrameY += description.Height; - currentFrameX = 0; - } + currentFrameX += description.Width; + if (currentFrameX >= imageWidth) { + currentFrameY += description.Height; + currentFrameX = 0; } } - - return description; } - private static string ParseString(string value) { - if (value.StartsWith("\"") && value.EndsWith("\"")) { - return value.Substring(1, value.Length - 2); - } else { - throw new Exception($"Invalid string in DMI description: {value}"); - } - } + return description; + } - private static bool VerifyPNG(Stream stream) { - byte[] header = new byte[PngHeader.Length]; - if (stream.Read(header, 0, header.Length) < header.Length) return false; + private static string ParseString(string value) { + if (value.StartsWith("\"") && value.EndsWith("\"")) { + return value.Substring(1, value.Length - 2); + } else { + throw new Exception($"Invalid string in DMI description: {value}"); + } + } - for (int i = 0; i < PngHeader.Length; i++) { - if (header[i] != PngHeader[i]) return false; - } + private static bool VerifyPNG(Stream stream) { + byte[] header = new byte[PngHeader.Length]; + if (stream.Read(header, 0, header.Length) < header.Length) return false; - return true; + for (int i = 0; i < PngHeader.Length; i++) { + if (header[i] != PngHeader[i]) return false; } - private static uint ReadBigEndianUint32(BinaryReader reader) { - byte[] bytes = reader.ReadBytes(4); - Array.Reverse(bytes); //Little to Big-Endian - return BitConverter.ToUInt32(bytes); - } + return true; + } + + private static uint ReadBigEndianUint32(BinaryReader reader) { + byte[] bytes = reader.ReadBytes(4); + Array.Reverse(bytes); //Little to Big-Endian + return BitConverter.ToUInt32(bytes); } } From 3ccb91691cf754c2c16e61710a49c2ea74cb9f30 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 27 Dec 2023 14:41:46 -0500 Subject: [PATCH 26/64] Avoid allocating shaders when values are default (#1570) * Avoid allocating shaders when values are default * Update some outdated comments --- OpenDream.sln.DotSettings | 1 + OpenDreamClient/Rendering/DreamPlane.cs | 2 +- OpenDreamClient/Rendering/DreamViewOverlay.cs | 24 ++++++++++++------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/OpenDream.sln.DotSettings b/OpenDream.sln.DotSettings index 4c33ccfb25..854dfd7752 100644 --- a/OpenDream.sln.DotSettings +++ b/OpenDream.sln.DotSettings @@ -48,6 +48,7 @@ True True True + True True True True diff --git a/OpenDreamClient/Rendering/DreamPlane.cs b/OpenDreamClient/Rendering/DreamPlane.cs index 4953727a90..fe728efd69 100644 --- a/OpenDreamClient/Rendering/DreamPlane.cs +++ b/OpenDreamClient/Rendering/DreamPlane.cs @@ -55,7 +55,7 @@ public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle) { if (_temporaryRenderTarget != null) { // Draw again, but with the color applied handle.RenderInRenderTarget(_temporaryRenderTarget, () => { - handle.UseShader(overlay.GetBlendAndColorShader(Master, blendModeOverride: BlendMode.Overlay)); + handle.UseShader(overlay.GetBlendAndColorShader(Master, useOverlayMode: true)); handle.SetTransform(overlay.CreateRenderTargetFlipMatrix(_temporaryRenderTarget.Size, Vector2.Zero)); handle.DrawTextureRect(_mainRenderTarget.Texture, new Box2(Vector2.Zero, _mainRenderTarget.Size)); handle.SetTransform(Matrix3.Identity); diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index e24977df9a..d26993c042 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -87,8 +87,8 @@ public DreamViewOverlay(TransformSystem transformSystem, EntityLookupSystem look _blockColorInstance = _protoManager.Index("blockcolor").InstanceUnique(); _colorInstance = _protoManager.Index("color").InstanceUnique(); _blendModeInstances = new(6) { - {BlendMode.Default, _protoManager.Index("blend_overlay").InstanceUnique()}, //BLEND_DEFAULT - {BlendMode.Overlay, _protoManager.Index("blend_overlay").InstanceUnique()}, //BLEND_OVERLAY (same as BLEND_DEFAULT) + {BlendMode.Default, _protoManager.Index("blend_overlay").InstanceUnique()}, //BLEND_DEFAULT (Same as BLEND_OVERLAY when there's no parent) + {BlendMode.Overlay, _protoManager.Index("blend_overlay").InstanceUnique()}, //BLEND_OVERLAY {BlendMode.Add, _protoManager.Index("blend_add").InstanceUnique()}, //BLEND_ADD {BlendMode.Subtract, _protoManager.Index("blend_subtract").InstanceUnique()}, //BLEND_SUBTRACT {BlendMode.Multiply, _protoManager.Index("blend_multiply").InstanceUnique()}, //BLEND_MULTIPLY @@ -379,16 +379,23 @@ private void ClearRenderTarget(IRenderTexture target, DrawingHandleWorld handle, handle.RenderInRenderTarget(target, () => {}, clearColor); } - public ShaderInstance GetBlendAndColorShader(RendererMetaData iconMetaData, Color? colorOverride = null, BlendMode? blendModeOverride = null) { - Color rgba = colorOverride ?? iconMetaData.ColorToApply.WithAlpha(iconMetaData.AlphaToApply); + public ShaderInstance? GetBlendAndColorShader(RendererMetaData iconMetaData, bool ignoreColor = false, bool useOverlayMode = false) { + BlendMode blendMode = useOverlayMode ? BlendMode.Overlay : iconMetaData.BlendMode; ColorMatrix colorMatrix; - if (colorOverride != null || iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity)) - colorMatrix = new ColorMatrix(rgba); + if (ignoreColor) + colorMatrix = ColorMatrix.Identity; + else if (iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity)) + colorMatrix = new ColorMatrix(iconMetaData.ColorToApply.WithAlpha(iconMetaData.AlphaToApply)); else colorMatrix = iconMetaData.ColorMatrixToApply; - if (!_blendModeInstances.TryGetValue(blendModeOverride ?? iconMetaData.BlendMode, out var blendAndColor)) + // We can use no shader if everything is default + if (!iconMetaData.IsPlaneMaster && blendMode is BlendMode.Default or BlendMode.Overlay && + colorMatrix.Equals(ColorMatrix.Identity)) + return null; + + if (!_blendModeInstances.TryGetValue(blendMode, out var blendAndColor)) blendAndColor = _blendModeInstances[BlendMode.Default]; blendAndColor = blendAndColor.Duplicate(); @@ -548,8 +555,7 @@ public ShaderInstance GetBlendAndColorShader(RendererMetaData iconMetaData, Colo //then we return the Action that draws the actual icon with filters applied iconDrawAction = renderTargetSize => { //note we apply the color *before* the filters, so we use override here - handle.UseShader(GetBlendAndColorShader(iconMetaData, colorOverride: Color.White)); - + handle.UseShader(GetBlendAndColorShader(iconMetaData, ignoreColor: true)); handle.SetTransform(CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition - frame.Size / 2)); handle.DrawTextureRect(pong.Texture, new Box2(Vector2.Zero, pong.Size)); handle.UseShader(null); From 173ca011fdc4e0725ae00303746466220579d7e4 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 28 Dec 2023 05:26:40 +0100 Subject: [PATCH 27/64] ACZ fixes (#1573) * Fix packaging * Fix ACZ root path being wrong if using relative JSON path The JSON path is relative to the original working directory of the file. However the server switches working directory during initialization of DreamResourceManager, which invalidates the old path. This old path was still passed to ACZ, so using a relative JSON path would make ACZ unable to find rsc files. The path is now turned absolute before the working directory switch occurs. * Add Full Hybrid ACZ provider. This permits rsc resources to be combined with the Content.Client.zip Hybrid ACZ. Also update RT * Update OpenDreamPackaging/DreamPackaging.cs --------- Co-authored-by: wixoa --- OpenDreamPackaging/DreamPackaging.cs | 17 ++++++--- OpenDreamRuntime/DreamAczProvider.cs | 43 +++++++++++++++++++++++ OpenDreamRuntime/DreamMagicAczProvider.cs | 26 -------------- OpenDreamRuntime/DreamManager.cs | 8 ++--- OpenDreamRuntime/EntryPoint.cs | 4 --- RobustToolbox | 2 +- 6 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 OpenDreamRuntime/DreamAczProvider.cs delete mode 100644 OpenDreamRuntime/DreamMagicAczProvider.cs diff --git a/OpenDreamPackaging/DreamPackaging.cs b/OpenDreamPackaging/DreamPackaging.cs index 13004e1e97..e171bb0208 100644 --- a/OpenDreamPackaging/DreamPackaging.cs +++ b/OpenDreamPackaging/DreamPackaging.cs @@ -1,5 +1,6 @@ using Robust.Packaging; using Robust.Packaging.AssetProcessing; +using Robust.Packaging.AssetProcessing.Passes; namespace OpenDreamPackaging; @@ -19,13 +20,21 @@ public static async Task WriteResources( var inputPass = graph.Input; - await RobustClientPackaging.WriteClientResources( - contentDir, + await RobustSharedPackaging.WriteContentAssemblies( inputPass, - cancel); + contentDir, + "Content.Client", + new[] { "OpenDreamClient", "OpenDreamShared" }, + cancel: cancel); await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel); + WriteRscResources(dreamRootDir, resources, inputPass); + + inputPass.InjectFinished(); + } + + public static void WriteRscResources(string dreamRootDir, string[] resources, AssetPassPipe inputPass) { for (var i = 0; i < resources.Length; i++) { var resource = resources[i].Replace('\\', Path.DirectorySeparatorChar); // The game client only knows a resource ID, so that's what we name the files. @@ -35,7 +44,5 @@ await RobustClientPackaging.WriteClientResources( inputPass.InjectFileFromDisk(path, diskPath); } - - inputPass.InjectFinished(); } } diff --git a/OpenDreamRuntime/DreamAczProvider.cs b/OpenDreamRuntime/DreamAczProvider.cs new file mode 100644 index 0000000000..c85182a556 --- /dev/null +++ b/OpenDreamRuntime/DreamAczProvider.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenDreamPackaging; +using Robust.Packaging; +using Robust.Packaging.AssetProcessing; +using Robust.Server.ServerStatus; + +namespace OpenDreamRuntime; + +public sealed class DreamAczProvider : IMagicAczProvider, IFullHybridAczProvider { + private readonly IDependencyCollection _dependencies; + private readonly string _rootPath; + private readonly string[] _resources; + + public DreamAczProvider(IDependencyCollection dependencies, string rootPath, string[] resources) { + _dependencies = dependencies; + _rootPath = rootPath; + _resources = resources; + } + + public async Task Package(AssetPass pass, IPackageLogger logger, CancellationToken cancel) { + var contentDir = DefaultMagicAczProvider.FindContentRootPath(_dependencies); + + await DreamPackaging.WriteResources(contentDir, _rootPath, _resources, pass, logger, cancel); + } + + public Task Package(AssetPass hybridPackageInput, AssetPass output, IPackageLogger logger, CancellationToken cancel) { + var clientAssetGraph = new RobustClientAssetGraph(); + var resourceInput = clientAssetGraph.Input; + output.AddDependency(clientAssetGraph.Output); + output.AddDependency(hybridPackageInput); + + AssetGraph.CalculateGraph( + clientAssetGraph.AllPasses.Concat(new[] { hybridPackageInput, output }).ToArray(), + logger); + + DreamPackaging.WriteRscResources(_rootPath, _resources, resourceInput); + resourceInput.InjectFinished(); + + return Task.CompletedTask; + } +} diff --git a/OpenDreamRuntime/DreamMagicAczProvider.cs b/OpenDreamRuntime/DreamMagicAczProvider.cs deleted file mode 100644 index 965f6aa8b6..0000000000 --- a/OpenDreamRuntime/DreamMagicAczProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using OpenDreamPackaging; -using Robust.Packaging; -using Robust.Packaging.AssetProcessing; -using Robust.Server.ServerStatus; - -namespace OpenDreamRuntime; - -public sealed class DreamMagicAczProvider : IMagicAczProvider { - private readonly IDependencyCollection _dependencies; - private readonly string _rootPath; - private readonly string[] _resources; - - public DreamMagicAczProvider(IDependencyCollection dependencies, string rootPath, string[] resources) { - _dependencies = dependencies; - _rootPath = rootPath; - _resources = resources; - } - - public async Task Package(AssetPass pass, IPackageLogger logger, CancellationToken cancel) { - var contentDir = DefaultMagicAczProvider.FindContentRootPath(_dependencies); - - await DreamPackaging.WriteResources(contentDir, _rootPath, _resources, pass, logger, cancel); - } -} diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 5befb8d19f..7453ae3c70 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -116,7 +116,7 @@ public bool LoadJson(string? jsonPath) { } _compiledJson = json; - var rootPath = Path.GetDirectoryName(jsonPath)!; + var rootPath = Path.GetFullPath(Path.GetDirectoryName(jsonPath)!); var resources = _compiledJson.Resources ?? Array.Empty(); _dreamResourceManager.Initialize(rootPath, resources); if(!string.IsNullOrEmpty(_compiledJson.Interface) && !_dreamResourceManager.DoesFileExist(_compiledJson.Interface)) @@ -146,9 +146,9 @@ public bool LoadJson(string? jsonPath) { _dreamMapManager.LoadMaps(_compiledJson.Maps); - _statusHost.SetMagicAczProvider(new DreamMagicAczProvider( - _dependencyCollection, rootPath, resources - )); + var aczProvider = new DreamAczProvider(_dependencyCollection, rootPath, resources); + _statusHost.SetMagicAczProvider(aczProvider); + _statusHost.SetFullHybridAczProvider(aczProvider); return true; } diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 6f962e645e..6a54f0db18 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -25,10 +25,6 @@ public sealed class EntryPoint : GameServer { private DreamCommandSystem? _commandSystem; public override void Init() { - IoCManager.Resolve().SetMagicAczProvider(new DefaultMagicAczProvider( - new DefaultMagicAczInfo("Content.Client", new[] {"OpenDreamClient", "OpenDreamShared"}), - IoCManager.Resolve())); - IComponentFactory componentFactory = IoCManager.Resolve(); componentFactory.DoAutoRegistrations(); diff --git a/RobustToolbox b/RobustToolbox index eb092e90ef..a891cacae5 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit eb092e90efc7ac4ae562bc46f9b760745a29e289 +Subproject commit a891cacae53d704f1d0afa85bd3bc10820c38881 From 5d0cad69fd42e439b48b8049707605ff0d31252b Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 28 Dec 2023 00:05:08 -0500 Subject: [PATCH 28/64] Revert "ACZ fixes (#1573)" (#1574) This reverts commit 173ca011fdc4e0725ae00303746466220579d7e4. --- OpenDreamPackaging/DreamPackaging.cs | 17 +++------ OpenDreamRuntime/DreamAczProvider.cs | 43 ----------------------- OpenDreamRuntime/DreamMagicAczProvider.cs | 26 ++++++++++++++ OpenDreamRuntime/DreamManager.cs | 8 ++--- OpenDreamRuntime/EntryPoint.cs | 4 +++ RobustToolbox | 2 +- 6 files changed, 40 insertions(+), 60 deletions(-) delete mode 100644 OpenDreamRuntime/DreamAczProvider.cs create mode 100644 OpenDreamRuntime/DreamMagicAczProvider.cs diff --git a/OpenDreamPackaging/DreamPackaging.cs b/OpenDreamPackaging/DreamPackaging.cs index e171bb0208..13004e1e97 100644 --- a/OpenDreamPackaging/DreamPackaging.cs +++ b/OpenDreamPackaging/DreamPackaging.cs @@ -1,6 +1,5 @@ using Robust.Packaging; using Robust.Packaging.AssetProcessing; -using Robust.Packaging.AssetProcessing.Passes; namespace OpenDreamPackaging; @@ -20,21 +19,13 @@ public static async Task WriteResources( var inputPass = graph.Input; - await RobustSharedPackaging.WriteContentAssemblies( - inputPass, + await RobustClientPackaging.WriteClientResources( contentDir, - "Content.Client", - new[] { "OpenDreamClient", "OpenDreamShared" }, - cancel: cancel); + inputPass, + cancel); await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel); - WriteRscResources(dreamRootDir, resources, inputPass); - - inputPass.InjectFinished(); - } - - public static void WriteRscResources(string dreamRootDir, string[] resources, AssetPassPipe inputPass) { for (var i = 0; i < resources.Length; i++) { var resource = resources[i].Replace('\\', Path.DirectorySeparatorChar); // The game client only knows a resource ID, so that's what we name the files. @@ -44,5 +35,7 @@ public static void WriteRscResources(string dreamRootDir, string[] resources, As inputPass.InjectFileFromDisk(path, diskPath); } + + inputPass.InjectFinished(); } } diff --git a/OpenDreamRuntime/DreamAczProvider.cs b/OpenDreamRuntime/DreamAczProvider.cs deleted file mode 100644 index c85182a556..0000000000 --- a/OpenDreamRuntime/DreamAczProvider.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using OpenDreamPackaging; -using Robust.Packaging; -using Robust.Packaging.AssetProcessing; -using Robust.Server.ServerStatus; - -namespace OpenDreamRuntime; - -public sealed class DreamAczProvider : IMagicAczProvider, IFullHybridAczProvider { - private readonly IDependencyCollection _dependencies; - private readonly string _rootPath; - private readonly string[] _resources; - - public DreamAczProvider(IDependencyCollection dependencies, string rootPath, string[] resources) { - _dependencies = dependencies; - _rootPath = rootPath; - _resources = resources; - } - - public async Task Package(AssetPass pass, IPackageLogger logger, CancellationToken cancel) { - var contentDir = DefaultMagicAczProvider.FindContentRootPath(_dependencies); - - await DreamPackaging.WriteResources(contentDir, _rootPath, _resources, pass, logger, cancel); - } - - public Task Package(AssetPass hybridPackageInput, AssetPass output, IPackageLogger logger, CancellationToken cancel) { - var clientAssetGraph = new RobustClientAssetGraph(); - var resourceInput = clientAssetGraph.Input; - output.AddDependency(clientAssetGraph.Output); - output.AddDependency(hybridPackageInput); - - AssetGraph.CalculateGraph( - clientAssetGraph.AllPasses.Concat(new[] { hybridPackageInput, output }).ToArray(), - logger); - - DreamPackaging.WriteRscResources(_rootPath, _resources, resourceInput); - resourceInput.InjectFinished(); - - return Task.CompletedTask; - } -} diff --git a/OpenDreamRuntime/DreamMagicAczProvider.cs b/OpenDreamRuntime/DreamMagicAczProvider.cs new file mode 100644 index 0000000000..965f6aa8b6 --- /dev/null +++ b/OpenDreamRuntime/DreamMagicAczProvider.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using OpenDreamPackaging; +using Robust.Packaging; +using Robust.Packaging.AssetProcessing; +using Robust.Server.ServerStatus; + +namespace OpenDreamRuntime; + +public sealed class DreamMagicAczProvider : IMagicAczProvider { + private readonly IDependencyCollection _dependencies; + private readonly string _rootPath; + private readonly string[] _resources; + + public DreamMagicAczProvider(IDependencyCollection dependencies, string rootPath, string[] resources) { + _dependencies = dependencies; + _rootPath = rootPath; + _resources = resources; + } + + public async Task Package(AssetPass pass, IPackageLogger logger, CancellationToken cancel) { + var contentDir = DefaultMagicAczProvider.FindContentRootPath(_dependencies); + + await DreamPackaging.WriteResources(contentDir, _rootPath, _resources, pass, logger, cancel); + } +} diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 7453ae3c70..5befb8d19f 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -116,7 +116,7 @@ public bool LoadJson(string? jsonPath) { } _compiledJson = json; - var rootPath = Path.GetFullPath(Path.GetDirectoryName(jsonPath)!); + var rootPath = Path.GetDirectoryName(jsonPath)!; var resources = _compiledJson.Resources ?? Array.Empty(); _dreamResourceManager.Initialize(rootPath, resources); if(!string.IsNullOrEmpty(_compiledJson.Interface) && !_dreamResourceManager.DoesFileExist(_compiledJson.Interface)) @@ -146,9 +146,9 @@ public bool LoadJson(string? jsonPath) { _dreamMapManager.LoadMaps(_compiledJson.Maps); - var aczProvider = new DreamAczProvider(_dependencyCollection, rootPath, resources); - _statusHost.SetMagicAczProvider(aczProvider); - _statusHost.SetFullHybridAczProvider(aczProvider); + _statusHost.SetMagicAczProvider(new DreamMagicAczProvider( + _dependencyCollection, rootPath, resources + )); return true; } diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 6a54f0db18..6f962e645e 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -25,6 +25,10 @@ public sealed class EntryPoint : GameServer { private DreamCommandSystem? _commandSystem; public override void Init() { + IoCManager.Resolve().SetMagicAczProvider(new DefaultMagicAczProvider( + new DefaultMagicAczInfo("Content.Client", new[] {"OpenDreamClient", "OpenDreamShared"}), + IoCManager.Resolve())); + IComponentFactory componentFactory = IoCManager.Resolve(); componentFactory.DoAutoRegistrations(); diff --git a/RobustToolbox b/RobustToolbox index a891cacae5..eb092e90ef 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit a891cacae53d704f1d0afa85bd3bc10820c38881 +Subproject commit eb092e90efc7ac4ae562bc46f9b760745a29e289 From 5194bcd4f20f5e3012efd2ac8f8626c625909089 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 28 Dec 2023 19:22:55 +0100 Subject: [PATCH 29/64] Fix Content.Client.zip name from packaging (#1576) It was "Zip" (capitalized) instead of "zip" so the server couldn't find it on a case-sensitive file system. --- OpenDreamPackageTool/ServerPackaging.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamPackageTool/ServerPackaging.cs b/OpenDreamPackageTool/ServerPackaging.cs index 1d38a0939c..bda62bde52 100644 --- a/OpenDreamPackageTool/ServerPackaging.cs +++ b/OpenDreamPackageTool/ServerPackaging.cs @@ -151,7 +151,7 @@ private static void BuildPlatform(PlatformReg platform, Program.ServerOptions op CopyContentAssemblies(Path.Combine(releaseDir, "Resources", "Assemblies")); if (options.HybridAcz) { // Hybrid ACZ expects "Content.Client.zip" (as it's not OpenDream-specific) - ZipFile.CreateFromDirectory(Path.Combine(options.OutputDir, "OpenDreamClient"), Path.Combine(releaseDir, "Content.Client.Zip")); + ZipFile.CreateFromDirectory(Path.Combine(options.OutputDir, "OpenDreamClient"), Path.Combine(releaseDir, "Content.Client.zip")); } } From 4434107cb161e9b7137215b6be80dc5998ce93ab Mon Sep 17 00:00:00 2001 From: wixoa Date: Sat, 30 Dec 2023 18:13:47 -0500 Subject: [PATCH 30/64] ACZ fixes and RT v198.1.0 (#1578) * ACZ fixes (#1573) * Fix packaging * Fix ACZ root path being wrong if using relative JSON path The JSON path is relative to the original working directory of the file. However the server switches working directory during initialization of DreamResourceManager, which invalidates the old path. This old path was still passed to ACZ, so using a relative JSON path would make ACZ unable to find rsc files. The path is now turned absolute before the working directory switch occurs. * Add Full Hybrid ACZ provider. This permits rsc resources to be combined with the Content.Client.zip Hybrid ACZ. Also update RT * Update OpenDreamPackaging/DreamPackaging.cs --------- Co-authored-by: wixoa (cherry picked from commit 173ca011fdc4e0725ae00303746466220579d7e4) * Update RT to v198.1.0 * Use file-scoped namespaces --------- Co-authored-by: Pieter-Jan Briers --- .../Rendering/ClientScreenOverlaySystem.cs | 38 ++++++++-------- OpenDreamPackaging/DreamPackaging.cs | 17 +++++--- OpenDreamRuntime/DreamAczProvider.cs | 43 +++++++++++++++++++ OpenDreamRuntime/DreamMagicAczProvider.cs | 26 ----------- OpenDreamRuntime/DreamManager.cs | 8 ++-- OpenDreamRuntime/EntryPoint.cs | 4 -- .../Rendering/ServerScreenOverlaySystem.cs | 41 ++++++------------ RobustToolbox | 2 +- 8 files changed, 92 insertions(+), 87 deletions(-) create mode 100644 OpenDreamRuntime/DreamAczProvider.cs delete mode 100644 OpenDreamRuntime/DreamMagicAczProvider.cs diff --git a/OpenDreamClient/Rendering/ClientScreenOverlaySystem.cs b/OpenDreamClient/Rendering/ClientScreenOverlaySystem.cs index 5a2bd05319..64dbfd85a5 100644 --- a/OpenDreamClient/Rendering/ClientScreenOverlaySystem.cs +++ b/OpenDreamClient/Rendering/ClientScreenOverlaySystem.cs @@ -1,28 +1,28 @@ using OpenDreamShared.Rendering; -namespace OpenDreamClient.Rendering { - sealed class ClientScreenOverlaySystem : SharedScreenOverlaySystem { - public HashSet ScreenObjects = new(); +namespace OpenDreamClient.Rendering; - [Dependency] private IEntityManager _entityManager = default!; +internal sealed class ClientScreenOverlaySystem : SharedScreenOverlaySystem { + public HashSet ScreenObjects = new(); - public override void Initialize() { - SubscribeNetworkEvent(OnAddScreenObject); - SubscribeNetworkEvent(OnRemoveScreenObject); - } + [Dependency] private readonly IEntityManager _entityManager = default!; - public override void Shutdown() { - ScreenObjects.Clear(); - } + public override void Initialize() { + SubscribeNetworkEvent(OnAddScreenObject); + SubscribeNetworkEvent(OnRemoveScreenObject); + } + + public override void Shutdown() { + ScreenObjects.Clear(); + } - private void OnAddScreenObject(AddScreenObjectEvent e) { - EntityUid ent = _entityManager.GetEntity(e.ScreenObject); - ScreenObjects.Add(ent); - } + private void OnAddScreenObject(AddScreenObjectEvent e) { + EntityUid ent = _entityManager.GetEntity(e.ScreenObject); + ScreenObjects.Add(ent); + } - private void OnRemoveScreenObject(RemoveScreenObjectEvent e) { - EntityUid ent = _entityManager.GetEntity(e.ScreenObject); - ScreenObjects.Remove(ent); - } + private void OnRemoveScreenObject(RemoveScreenObjectEvent e) { + EntityUid ent = _entityManager.GetEntity(e.ScreenObject); + ScreenObjects.Remove(ent); } } diff --git a/OpenDreamPackaging/DreamPackaging.cs b/OpenDreamPackaging/DreamPackaging.cs index 13004e1e97..e171bb0208 100644 --- a/OpenDreamPackaging/DreamPackaging.cs +++ b/OpenDreamPackaging/DreamPackaging.cs @@ -1,5 +1,6 @@ using Robust.Packaging; using Robust.Packaging.AssetProcessing; +using Robust.Packaging.AssetProcessing.Passes; namespace OpenDreamPackaging; @@ -19,13 +20,21 @@ public static async Task WriteResources( var inputPass = graph.Input; - await RobustClientPackaging.WriteClientResources( - contentDir, + await RobustSharedPackaging.WriteContentAssemblies( inputPass, - cancel); + contentDir, + "Content.Client", + new[] { "OpenDreamClient", "OpenDreamShared" }, + cancel: cancel); await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel); + WriteRscResources(dreamRootDir, resources, inputPass); + + inputPass.InjectFinished(); + } + + public static void WriteRscResources(string dreamRootDir, string[] resources, AssetPassPipe inputPass) { for (var i = 0; i < resources.Length; i++) { var resource = resources[i].Replace('\\', Path.DirectorySeparatorChar); // The game client only knows a resource ID, so that's what we name the files. @@ -35,7 +44,5 @@ await RobustClientPackaging.WriteClientResources( inputPass.InjectFileFromDisk(path, diskPath); } - - inputPass.InjectFinished(); } } diff --git a/OpenDreamRuntime/DreamAczProvider.cs b/OpenDreamRuntime/DreamAczProvider.cs new file mode 100644 index 0000000000..c85182a556 --- /dev/null +++ b/OpenDreamRuntime/DreamAczProvider.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenDreamPackaging; +using Robust.Packaging; +using Robust.Packaging.AssetProcessing; +using Robust.Server.ServerStatus; + +namespace OpenDreamRuntime; + +public sealed class DreamAczProvider : IMagicAczProvider, IFullHybridAczProvider { + private readonly IDependencyCollection _dependencies; + private readonly string _rootPath; + private readonly string[] _resources; + + public DreamAczProvider(IDependencyCollection dependencies, string rootPath, string[] resources) { + _dependencies = dependencies; + _rootPath = rootPath; + _resources = resources; + } + + public async Task Package(AssetPass pass, IPackageLogger logger, CancellationToken cancel) { + var contentDir = DefaultMagicAczProvider.FindContentRootPath(_dependencies); + + await DreamPackaging.WriteResources(contentDir, _rootPath, _resources, pass, logger, cancel); + } + + public Task Package(AssetPass hybridPackageInput, AssetPass output, IPackageLogger logger, CancellationToken cancel) { + var clientAssetGraph = new RobustClientAssetGraph(); + var resourceInput = clientAssetGraph.Input; + output.AddDependency(clientAssetGraph.Output); + output.AddDependency(hybridPackageInput); + + AssetGraph.CalculateGraph( + clientAssetGraph.AllPasses.Concat(new[] { hybridPackageInput, output }).ToArray(), + logger); + + DreamPackaging.WriteRscResources(_rootPath, _resources, resourceInput); + resourceInput.InjectFinished(); + + return Task.CompletedTask; + } +} diff --git a/OpenDreamRuntime/DreamMagicAczProvider.cs b/OpenDreamRuntime/DreamMagicAczProvider.cs deleted file mode 100644 index 965f6aa8b6..0000000000 --- a/OpenDreamRuntime/DreamMagicAczProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using OpenDreamPackaging; -using Robust.Packaging; -using Robust.Packaging.AssetProcessing; -using Robust.Server.ServerStatus; - -namespace OpenDreamRuntime; - -public sealed class DreamMagicAczProvider : IMagicAczProvider { - private readonly IDependencyCollection _dependencies; - private readonly string _rootPath; - private readonly string[] _resources; - - public DreamMagicAczProvider(IDependencyCollection dependencies, string rootPath, string[] resources) { - _dependencies = dependencies; - _rootPath = rootPath; - _resources = resources; - } - - public async Task Package(AssetPass pass, IPackageLogger logger, CancellationToken cancel) { - var contentDir = DefaultMagicAczProvider.FindContentRootPath(_dependencies); - - await DreamPackaging.WriteResources(contentDir, _rootPath, _resources, pass, logger, cancel); - } -} diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 5befb8d19f..7453ae3c70 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -116,7 +116,7 @@ public bool LoadJson(string? jsonPath) { } _compiledJson = json; - var rootPath = Path.GetDirectoryName(jsonPath)!; + var rootPath = Path.GetFullPath(Path.GetDirectoryName(jsonPath)!); var resources = _compiledJson.Resources ?? Array.Empty(); _dreamResourceManager.Initialize(rootPath, resources); if(!string.IsNullOrEmpty(_compiledJson.Interface) && !_dreamResourceManager.DoesFileExist(_compiledJson.Interface)) @@ -146,9 +146,9 @@ public bool LoadJson(string? jsonPath) { _dreamMapManager.LoadMaps(_compiledJson.Maps); - _statusHost.SetMagicAczProvider(new DreamMagicAczProvider( - _dependencyCollection, rootPath, resources - )); + var aczProvider = new DreamAczProvider(_dependencyCollection, rootPath, resources); + _statusHost.SetMagicAczProvider(aczProvider); + _statusHost.SetFullHybridAczProvider(aczProvider); return true; } diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 6f962e645e..6a54f0db18 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -25,10 +25,6 @@ public sealed class EntryPoint : GameServer { private DreamCommandSystem? _commandSystem; public override void Init() { - IoCManager.Resolve().SetMagicAczProvider(new DefaultMagicAczProvider( - new DefaultMagicAczInfo("Content.Client", new[] {"OpenDreamClient", "OpenDreamShared"}), - IoCManager.Resolve())); - IComponentFactory componentFactory = IoCManager.Resolve(); componentFactory.DoAutoRegistrations(); diff --git a/OpenDreamRuntime/Rendering/ServerScreenOverlaySystem.cs b/OpenDreamRuntime/Rendering/ServerScreenOverlaySystem.cs index aefe7f0b2d..64dd1ed25c 100644 --- a/OpenDreamRuntime/Rendering/ServerScreenOverlaySystem.cs +++ b/OpenDreamRuntime/Rendering/ServerScreenOverlaySystem.cs @@ -1,39 +1,24 @@ using OpenDreamRuntime.Objects.Types; using OpenDreamShared.Rendering; using Robust.Server.GameStates; -using Robust.Shared.Player; -namespace OpenDreamRuntime.Rendering { - public sealed class ServerScreenOverlaySystem : SharedScreenOverlaySystem { - private readonly Dictionary> _sessionToScreenObjects = new(); - [Dependency] private readonly IEntityManager _entityManager = default!; +namespace OpenDreamRuntime.Rendering; - public override void Initialize() { - SubscribeLocalEvent(HandleExpandPvsEvent); - } +public sealed class ServerScreenOverlaySystem : SharedScreenOverlaySystem { + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly PvsOverrideSystem _pvsOverride = default!; - public void AddScreenObject(DreamConnection connection, DreamObjectMovable screenObject) { - if (!_sessionToScreenObjects.TryGetValue(connection.Session, out var objects)) { - objects = new HashSet(); - _sessionToScreenObjects.Add(connection.Session, objects); - } + public void AddScreenObject(DreamConnection connection, DreamObjectMovable screenObject) { + _pvsOverride.AddForceSend(screenObject.Entity, connection.Session); - objects.Add(screenObject.Entity); - NetEntity ent = _entityManager.GetNetEntity(screenObject.Entity); - RaiseNetworkEvent(new AddScreenObjectEvent(ent), connection.Session.ConnectedClient); - } + NetEntity ent = _entityManager.GetNetEntity(screenObject.Entity); + RaiseNetworkEvent(new AddScreenObjectEvent(ent), connection.Session.ConnectedClient); + } - public void RemoveScreenObject(DreamConnection connection, DreamObjectMovable screenObject) { - _sessionToScreenObjects[connection.Session].Remove(screenObject.Entity); - NetEntity ent = _entityManager.GetNetEntity(screenObject.Entity); - RaiseNetworkEvent(new RemoveScreenObjectEvent(ent), connection.Session.ConnectedClient); - } + public void RemoveScreenObject(DreamConnection connection, DreamObjectMovable screenObject) { + _pvsOverride.RemoveForceSend(screenObject.Entity, connection.Session); - private void HandleExpandPvsEvent(ref ExpandPvsEvent e) { - if (_sessionToScreenObjects.TryGetValue(e.Session, out var objects)) { - e.Entities ??= new(objects.Count); - e.Entities.AddRange(objects); - } - } + NetEntity ent = _entityManager.GetNetEntity(screenObject.Entity); + RaiseNetworkEvent(new RemoveScreenObjectEvent(ent), connection.Session.ConnectedClient); } } diff --git a/RobustToolbox b/RobustToolbox index eb092e90ef..73357f022b 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit eb092e90efc7ac4ae562bc46f9b760745a29e289 +Subproject commit 73357f022ba3a0b60587b602f68e160df6d79648 From 186d10dc0268ccc2861ef610ab5db856af6fa716 Mon Sep 17 00:00:00 2001 From: ike709 Date: Sat, 30 Dec 2023 16:45:01 -0700 Subject: [PATCH 31/64] Fix gliding (#1575) Co-authored-by: ike709 --- OpenDreamClient/Rendering/AtomGlideSystem.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/OpenDreamClient/Rendering/AtomGlideSystem.cs b/OpenDreamClient/Rendering/AtomGlideSystem.cs index 7f2ab66edd..8320395506 100644 --- a/OpenDreamClient/Rendering/AtomGlideSystem.cs +++ b/OpenDreamClient/Rendering/AtomGlideSystem.cs @@ -33,7 +33,8 @@ public Glide(TransformComponent transform) { public override void Initialize() { _spriteQuery = _entityManager.GetEntityQuery(); - SubscribeLocalEvent(OnTransformMove); + + _transformSystem.OnGlobalMoveEvent += OnTransformMove; } public override void Shutdown() { @@ -80,10 +81,10 @@ public override void FrameUpdate(float frameTime) { /// /// Disables RT lerping and sets up the entity's glide /// - private void OnTransformMove(EntityUid entity, TransformComponent transform, ref MoveEvent e) { + private void OnTransformMove(ref MoveEvent e) { if (_ignoreMoveEvent || e.ParentChanged) return; - if (!_spriteQuery.TryGetComponent(entity, out var sprite)) + if (!_spriteQuery.TryGetComponent(e.Sender, out var sprite)) return; _ignoreMoveEvent = true; @@ -91,7 +92,7 @@ private void OnTransformMove(EntityUid entity, TransformComponent transform, ref // Look for any in-progress glides on this transform Glide? glide = null; foreach (var potentiallyThisTransform in _currentGlides) { - if (potentiallyThisTransform.Transform != transform) + if (potentiallyThisTransform.Transform != e.Component) continue; glide = potentiallyThisTransform; @@ -113,13 +114,13 @@ private void OnTransformMove(EntityUid entity, TransformComponent transform, ref } if (glide == null) { - glide = new(transform); + glide = new(e.Component); _currentGlides.Add(glide); } // Move the transform to our starting point // Also serves the function of disabling RT's lerp - _transformSystem.SetLocalPositionNoLerp(transform, startingFrom); + _transformSystem.SetLocalPositionNoLerp(e.Sender, startingFrom, e.Component); glide.EndPos = glidingTo; glide.MovementPerFrame = CalculateMovementPerFrame(sprite.Icon.Appearance.GlideSize); From a9bf0405c2a1156d2fd1df0cff05f2c22f6ed902 Mon Sep 17 00:00:00 2001 From: ike709 Date: Sat, 30 Dec 2023 18:29:40 -0700 Subject: [PATCH 32/64] Ports infolinks (#1579) * Ports infolinks * Update OpenDreamRuntime/ServerInfoManager.cs * Update OpenDreamRuntime/ServerInfoManager.cs * breh --------- Co-authored-by: ike709 --- OpenDreamRuntime/EntryPoint.cs | 3 +++ OpenDreamRuntime/ServerContentIoC.cs | 1 + OpenDreamRuntime/ServerInfoManager.cs | 38 +++++++++++++++++++++++++++ OpenDreamShared/OpenDreamCVars.cs | 34 ++++++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 OpenDreamRuntime/ServerInfoManager.cs diff --git a/OpenDreamRuntime/EntryPoint.cs b/OpenDreamRuntime/EntryPoint.cs index 6a54f0db18..be24d5dfd6 100644 --- a/OpenDreamRuntime/EntryPoint.cs +++ b/OpenDreamRuntime/EntryPoint.cs @@ -21,6 +21,7 @@ public sealed class EntryPoint : GameServer { [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IDreamDebugManager _debugManager = default!; + [Dependency] private readonly ServerInfoManager _serverInfoManager = default!; private DreamCommandSystem? _commandSystem; @@ -51,6 +52,8 @@ public override void Init() { } _prototypeManager.LoadDirectory(new ResPath("/Resources/Prototypes")); + + _serverInfoManager.Initialize(); } public override void PostInit() { diff --git a/OpenDreamRuntime/ServerContentIoC.cs b/OpenDreamRuntime/ServerContentIoC.cs index 20a2ed95bb..77ec1706bf 100644 --- a/OpenDreamRuntime/ServerContentIoC.cs +++ b/OpenDreamRuntime/ServerContentIoC.cs @@ -13,6 +13,7 @@ public static void Register(bool unitTests = false) { IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); #if DEBUG IoCManager.Register(); diff --git a/OpenDreamRuntime/ServerInfoManager.cs b/OpenDreamRuntime/ServerInfoManager.cs new file mode 100644 index 0000000000..aceb94430b --- /dev/null +++ b/OpenDreamRuntime/ServerInfoManager.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Nodes; +using OpenDreamShared; +using Robust.Server.ServerStatus; +using Robust.Shared.Configuration; + +namespace OpenDreamRuntime; + +/// +/// Adds additional data like info links to the server info endpoint +/// +public sealed class ServerInfoManager { + private static readonly (CVarDef cVar, string icon, string name)[] Vars = { + // @formatter:off + (OpenDreamCVars.InfoLinksDiscord, "discord", "Discord"), + (OpenDreamCVars.InfoLinksForum, "forum", "Forum"), + (OpenDreamCVars.InfoLinksGithub, "github", "GitHub"), + (OpenDreamCVars.InfoLinksWebsite, "web", "Website"), + (OpenDreamCVars.InfoLinksWiki, "wiki", "Wiki") + // @formatter:on + }; + + [Dependency] private readonly IStatusHost _statusHost = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public void Initialize() { + _statusHost.OnInfoRequest += OnInfoRequest; + } + + private void OnInfoRequest(JsonNode json) { + foreach (var (cVar, icon, name) in Vars) { + var url = _cfg.GetCVar(cVar); + if (string.IsNullOrEmpty(url)) + continue; + + StatusHostHelpers.AddLink(json, name, url, icon); + } + } +} diff --git a/OpenDreamShared/OpenDreamCVars.cs b/OpenDreamShared/OpenDreamCVars.cs index 3af2886caf..cc87102218 100644 --- a/OpenDreamShared/OpenDreamCVars.cs +++ b/OpenDreamShared/OpenDreamCVars.cs @@ -24,5 +24,39 @@ public abstract class OpenDreamCVars { public static readonly CVarDef TopicPort = CVarDef.Create("opendream.topic_port", 25567, CVar.SERVERONLY); + + /* + * INFOLINKS + */ + + /// + /// Link to Discord server to show in the launcher. + /// + public static readonly CVarDef InfoLinksDiscord = + CVarDef.Create("infolinks.discord", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to forum to show in the launcher. + /// + public static readonly CVarDef InfoLinksForum = + CVarDef.Create("infolinks.forum", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to GitHub page to show in the launcher. + /// + public static readonly CVarDef InfoLinksGithub = + CVarDef.Create("infolinks.github", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to website to show in the launcher. + /// + public static readonly CVarDef InfoLinksWebsite = + CVarDef.Create("infolinks.website", "", CVar.SERVER | CVar.REPLICATED); + + /// + /// Link to wiki to show in the launcher. + /// + public static readonly CVarDef InfoLinksWiki = + CVarDef.Create("infolinks.wiki", "", CVar.SERVER | CVar.REPLICATED); } } From 94ceabacf03c8fc04ff71a4199e8d1db2710dcfd Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 1 Jan 2024 01:23:26 -0500 Subject: [PATCH 33/64] Don't multiply alpha when blending onto a transparent pixel (#1577) --- OpenDreamRuntime/Objects/DreamIcon.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OpenDreamRuntime/Objects/DreamIcon.cs b/OpenDreamRuntime/Objects/DreamIcon.cs index ec954527c9..4d8320433c 100644 --- a/OpenDreamRuntime/Objects/DreamIcon.cs +++ b/OpenDreamRuntime/Objects/DreamIcon.cs @@ -333,6 +333,14 @@ protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) { } case BlendType.Overlay: { + // When overlaying onto 0 alpha, don't multiply the RGB values by alpha. + if (dst.A == 0) { + pixels[dstPixelPosition].R = src.R; + pixels[dstPixelPosition].G = src.G; + pixels[dstPixelPosition].B = src.B; + pixels[dstPixelPosition].A = src.A; + break; + } pixels[dstPixelPosition].R = (byte) (dst.R + (src.R - dst.R) * src.A / 255); pixels[dstPixelPosition].G = (byte) (dst.G + (src.G - dst.G) * src.A / 255); pixels[dstPixelPosition].B = (byte) (dst.B + (src.B - dst.B) * src.A / 255); From f63978a5fb89d45fdf293d728c96fba8124e7538 Mon Sep 17 00:00:00 2001 From: wixoa Date: Mon, 1 Jan 2024 01:34:22 -0500 Subject: [PATCH 34/64] Don't release planes with a plane master (#1599) * Don't release planes with a plane master * Properly dispose of the plane's render target --- OpenDreamClient/Rendering/DreamPlane.cs | 27 +++++++++---------- OpenDreamClient/Rendering/DreamViewOverlay.cs | 4 ++- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamPlane.cs b/OpenDreamClient/Rendering/DreamPlane.cs index fe728efd69..1932e4d6c5 100644 --- a/OpenDreamClient/Rendering/DreamPlane.cs +++ b/OpenDreamClient/Rendering/DreamPlane.cs @@ -1,23 +1,17 @@ -using OpenDreamShared.Dream; -using Robust.Client.Graphics; +using Robust.Client.Graphics; using Robust.Shared.Utility; namespace OpenDreamClient.Rendering; -internal sealed class DreamPlane { - public IRenderTexture RenderTarget => _temporaryRenderTarget ?? _mainRenderTarget; +internal sealed class DreamPlane(IRenderTexture mainRenderTarget) : IDisposable { + public IRenderTexture RenderTarget => _temporaryRenderTarget ?? mainRenderTarget; public RendererMetaData? Master; public readonly List> IconDrawActions = new(); public readonly List> MouseMapDrawActions = new(); - private IRenderTexture _mainRenderTarget; private IRenderTexture? _temporaryRenderTarget; - public DreamPlane(IRenderTexture renderTarget) { - _mainRenderTarget = renderTarget; - } - public void Clear() { Master = null; IconDrawActions.Clear(); @@ -25,13 +19,18 @@ public void Clear() { _temporaryRenderTarget = null; } + public void Dispose() { + mainRenderTarget.Dispose(); + Clear(); + } + /// /// Sets this plane's main render target
/// Persists through calls to ///
public void SetMainRenderTarget(IRenderTexture renderTarget) { - _mainRenderTarget.Dispose(); - _mainRenderTarget = renderTarget; + mainRenderTarget.Dispose(); + mainRenderTarget = renderTarget; } /// @@ -47,9 +46,9 @@ public void SetTemporaryRenderTarget(IRenderTexture renderTarget) { /// public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle) { // Draw all icons - handle.RenderInRenderTarget(_mainRenderTarget, () => { + handle.RenderInRenderTarget(mainRenderTarget, () => { foreach (Action iconAction in IconDrawActions) - iconAction(_mainRenderTarget.Size); + iconAction(mainRenderTarget.Size); }, new Color()); if (_temporaryRenderTarget != null) { @@ -57,7 +56,7 @@ public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle) { handle.RenderInRenderTarget(_temporaryRenderTarget, () => { handle.UseShader(overlay.GetBlendAndColorShader(Master, useOverlayMode: true)); handle.SetTransform(overlay.CreateRenderTargetFlipMatrix(_temporaryRenderTarget.Size, Vector2.Zero)); - handle.DrawTextureRect(_mainRenderTarget.Texture, new Box2(Vector2.Zero, _mainRenderTarget.Size)); + handle.DrawTextureRect(mainRenderTarget.Texture, new Box2(Vector2.Zero, mainRenderTarget.Size)); handle.SetTransform(Matrix3.Identity); handle.UseShader(null); }, new Color()); diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index d26993c042..89ba23b6b6 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -608,7 +608,8 @@ private void ClearPlanes() { var plane = pair.Value; // We can remove the plane if there was nothing on it last frame - if (plane.IconDrawActions.Count == 0 && plane.MouseMapDrawActions.Count == 0) { + if (plane.IconDrawActions.Count == 0 && plane.MouseMapDrawActions.Count == 0 && plane.Master == null) { + plane.Dispose(); _planes.Remove(pair.Key); continue; } @@ -625,6 +626,7 @@ private DreamPlane GetPlane(int planeIndex, Vector2i viewportSize) { plane = new(renderTarget); _planes.Add(planeIndex, plane); + _sawmill.Info($"Created plane {planeIndex}"); return plane; } From c1d4fabdd616ebd196ece19d8056ef4d5e5247e7 Mon Sep 17 00:00:00 2001 From: wixoa Date: Mon, 1 Jan 2024 09:19:12 -0500 Subject: [PATCH 35/64] Improve gliding at non-60FPS (#1600) * Use the frame delta when moving glides * Change `MovementPerFrame` name to `MovementSpeed` --- OpenDreamClient/Rendering/AtomGlideSystem.cs | 33 +++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/OpenDreamClient/Rendering/AtomGlideSystem.cs b/OpenDreamClient/Rendering/AtomGlideSystem.cs index 8320395506..a0b072132a 100644 --- a/OpenDreamClient/Rendering/AtomGlideSystem.cs +++ b/OpenDreamClient/Rendering/AtomGlideSystem.cs @@ -8,14 +8,10 @@ namespace OpenDreamClient.Rendering; /// Disables RobustToolbox's transform lerping and replaces it with our own gliding /// public sealed class AtomGlideSystem : EntitySystem { - private sealed class Glide { - public readonly TransformComponent Transform; + private sealed class Glide(TransformComponent transform) { + public readonly TransformComponent Transform = transform; public Vector2 EndPos; - public float MovementPerFrame; - - public Glide(TransformComponent transform) { - Transform = transform; - } + public float MovementSpeed; } [Dependency] private readonly TransformSystem _transformSystem = default!; @@ -48,21 +44,22 @@ public override void FrameUpdate(float frameTime) { var glide = _currentGlides[i]; var currentPos = glide.Transform.LocalPosition; var newPos = currentPos; + var movement = glide.MovementSpeed * frameTime; // Move X towards the end position at a constant speed if (!MathHelper.CloseTo(currentPos.X, glide.EndPos.X)) { if (currentPos.X < glide.EndPos.X) - newPos.X = Math.Min(glide.EndPos.X, newPos.X + glide.MovementPerFrame); + newPos.X = Math.Min(glide.EndPos.X, newPos.X + movement); else if (currentPos.X > glide.EndPos.X) - newPos.X = Math.Max(glide.EndPos.X, newPos.X - glide.MovementPerFrame); + newPos.X = Math.Max(glide.EndPos.X, newPos.X - movement); } // Move Y towards the end position at a constant speed if (!MathHelper.CloseTo(currentPos.Y, glide.EndPos.Y)) { if (currentPos.Y < glide.EndPos.Y) - newPos.Y = Math.Min(glide.EndPos.Y, newPos.Y + glide.MovementPerFrame); + newPos.Y = Math.Min(glide.EndPos.Y, newPos.Y + movement); else if (currentPos.Y > glide.EndPos.Y) - newPos.Y = Math.Max(glide.EndPos.Y, newPos.Y - glide.MovementPerFrame); + newPos.Y = Math.Max(glide.EndPos.Y, newPos.Y - movement); } if (newPos.EqualsApprox(glide.EndPos)) { // Glide is finished @@ -84,7 +81,7 @@ public override void FrameUpdate(float frameTime) { private void OnTransformMove(ref MoveEvent e) { if (_ignoreMoveEvent || e.ParentChanged) return; - if (!_spriteQuery.TryGetComponent(e.Sender, out var sprite)) + if (!_spriteQuery.TryGetComponent(e.Sender, out var sprite) || sprite.Icon?.Appearance is null) return; _ignoreMoveEvent = true; @@ -123,18 +120,16 @@ private void OnTransformMove(ref MoveEvent e) { _transformSystem.SetLocalPositionNoLerp(e.Sender, startingFrom, e.Component); glide.EndPos = glidingTo; - glide.MovementPerFrame = CalculateMovementPerFrame(sprite.Icon.Appearance.GlideSize); + glide.MovementSpeed = CalculateMovementSpeed(sprite.Icon.Appearance.GlideSize); _ignoreMoveEvent = false; } - private static float CalculateMovementPerFrame(byte glideSize) { + private static float CalculateMovementSpeed(byte glideSize) { if (glideSize == 0) glideSize = 4; // TODO: 0 gives us "automated control" over this value, not just setting it to 4 - // Assume a 60 FPS client and a 20 TPS server - // TODO: Support other FPS and TPS - var scaling = (60f / 20f); - - return glideSize / scaling / EyeManager.PixelsPerMeter; + // Assume a 20 TPS server + // TODO: Support other TPS + return (float)glideSize / EyeManager.PixelsPerMeter * 20f; } } From fa342876ea0b4f5f500371243e633bb3bf995be5 Mon Sep 17 00:00:00 2001 From: Zonespace <41448081+Zonespace27@users.noreply.github.com> Date: Mon, 1 Jan 2024 13:30:38 -0800 Subject: [PATCH 36/64] Adds a warning when assigning a value in a conditional (#1596) * adds a warning when assigning a value in a conditional * review request --- DMCompiler/Compiler/DM/DMParser.cs | 7 ++++++- DMCompiler/DMStandard/DefaultPragmaConfig.dm | 2 +- OpenDreamShared/Compiler/CompilerError.cs | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index d9c2554a89..a163037474 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -1065,7 +1065,12 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { Consume(TokenType.DM_LeftParenthesis, "Expected '('"); BracketWhitespace(); DMASTExpression? condition = Expression(); - if (condition == null) Error("Expected a condition"); + if (condition == null) { + Error("Expected a condition"); + } + if (condition is DMASTAssign) { + DMCompiler.Emit(WarningCode.AssignmentInConditional, condition.Location, "Assignment in conditional"); + } BracketWhitespace(); ConsumeRightParenthesis(); Whitespace(); diff --git a/DMCompiler/DMStandard/DefaultPragmaConfig.dm b/DMCompiler/DMStandard/DefaultPragmaConfig.dm index e256ae41a6..5b97c1dbbe 100644 --- a/DMCompiler/DMStandard/DefaultPragmaConfig.dm +++ b/DMCompiler/DMStandard/DefaultPragmaConfig.dm @@ -33,4 +33,4 @@ #pragma EmptyBlock notice #pragma EmptyProc disabled // NOTE: If you enable this in OD's default pragma config file, it will emit for OD's DMStandard. Put it in your codebase's pragma config file. #pragma UnsafeClientAccess disabled // NOTE: Only checks for unsafe accesses like "client.foobar" and doesn't consider if the client was already null-checked earlier in the proc - +#pragma AssignmentInConditional warning diff --git a/OpenDreamShared/Compiler/CompilerError.cs b/OpenDreamShared/Compiler/CompilerError.cs index 10b15cb7c9..d5f9e299d3 100644 --- a/OpenDreamShared/Compiler/CompilerError.cs +++ b/OpenDreamShared/Compiler/CompilerError.cs @@ -59,6 +59,7 @@ public enum WarningCode { EmptyProc = 3101, UnsafeClientAccess = 3200, SuspiciousSwitchCase = 3201, // "else if" cases are actually valid DM, they just spontaneously end the switch context and begin an if-else ladder within the else case of the switch + AssignmentInConditional = 3202, // 4000 - 4999 are reserved for runtime configuration. (TODO: Runtime doesn't know about configs yet!) } From a143b685566527dd464690231e831ee88e1691f0 Mon Sep 17 00:00:00 2001 From: Lucy Date: Mon, 1 Jan 2024 23:22:55 -0500 Subject: [PATCH 37/64] Implement color inputting (#1595) * Implement color inputting * Update OpenDreamRuntime/DreamConnection.cs Co-authored-by: wixoa * bwah --------- Co-authored-by: wixoa --- .../Interface/DreamInterfaceManager.cs | 60 +++--- .../Interface/Prompts/ColorPrompt.cs | 175 ++++++++++++++++++ OpenDreamRuntime/DreamConnection.cs | 8 +- .../Network/Messages/MsgPromptResponse.cs | 17 +- .../Textures/Interface/Nano/slider_fill.svg | 92 +++++++++ .../Interface/Nano/slider_fill.svg.96dpi.png | Bin 0 -> 267 bytes .../Interface/Nano/slider_grabber.svg | 89 +++++++++ .../Nano/slider_grabber.svg.96dpi.png | Bin 0 -> 285 bytes .../Interface/Nano/slider_outline.svg | 91 +++++++++ .../Nano/slider_outline.svg.96dpi.png | Bin 0 -> 284 bytes 10 files changed, 496 insertions(+), 36 deletions(-) create mode 100644 OpenDreamClient/Interface/Prompts/ColorPrompt.cs create mode 100644 Resources/Textures/Interface/Nano/slider_fill.svg create mode 100644 Resources/Textures/Interface/Nano/slider_fill.svg.96dpi.png create mode 100644 Resources/Textures/Interface/Nano/slider_grabber.svg create mode 100644 Resources/Textures/Interface/Nano/slider_grabber.svg.96dpi.png create mode 100644 Resources/Textures/Interface/Nano/slider_outline.svg create mode 100644 Resources/Textures/Interface/Nano/slider_outline.svg.96dpi.png diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index e82df03b06..d39e65a149 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -215,6 +215,8 @@ void OnPromptClose(DMValueType responseType, object? response) { prompt = new NumberPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); } else if ((pPrompt.Types & DMValueType.Message) == DMValueType.Message) { prompt = new MessagePrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); + } else if ((pPrompt.Types & DMValueType.Color) == DMValueType.Color) { + prompt = new ColorPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); } if (prompt != null) { @@ -368,7 +370,7 @@ public void FrameUpdate(FrameEventArgs frameEventArgs) { } else if (_popupWindows.TryGetValue(windowId, out var popup)) { window = popup.WindowElement; } else if (Menus.TryGetValue(windowId, out var menu)) { - if(menu.MenuElements.TryGetValue(elementId, out var menuElement)) + if (menu.MenuElements.TryGetValue(elementId, out var menuElement)) return menuElement; } @@ -426,45 +428,45 @@ public void SaveScreenshot(bool openDialog) { }); } - public void RunCommand(string command){ + public void RunCommand(string command) { switch (command) { - case string x when x.StartsWith(".quit"): - IoCManager.Resolve().ClientDisconnect(".quit used"); - break; + case string x when x.StartsWith(".quit"): + IoCManager.Resolve().ClientDisconnect(".quit used"); + break; - case string x when x.StartsWith(".screenshot"): - string[] split = command.Split(" "); - SaveScreenshot(split.Length == 1 || split[1] != "auto"); - break; + case string x when x.StartsWith(".screenshot"): + string[] split = command.Split(" "); + SaveScreenshot(split.Length == 1 || split[1] != "auto"); + break; - case string x when x.StartsWith(".configure"): - _sawmill.Warning(".configure command is not implemented"); - break; + case string x when x.StartsWith(".configure"): + _sawmill.Warning(".configure command is not implemented"); + break; - case string x when x.StartsWith(".winset"): - // Everything after .winset, excluding the space and quotes - string winsetParams = command.Substring(7); //clip .winset - winsetParams = winsetParams.Trim(); //clip space - winsetParams = winsetParams.Trim('\"'); //clip quotes + case string x when x.StartsWith(".winset"): + // Everything after .winset, excluding the space and quotes + string winsetParams = command.Substring(7); //clip .winset + winsetParams = winsetParams.Trim(); //clip space + winsetParams = winsetParams.Trim('\"'); //clip quotes - WinSet(null, winsetParams); - break; + WinSet(null, winsetParams); + break; - default: { - // Send the entire command to the server. - // It has more info about argument types so it can parse it better than we can. - _netManager.ClientSendMessage(new MsgCommand(){Command = command}); - break; - } + default: { + // Send the entire command to the server. + // It has more info about argument types so it can parse it better than we can. + _netManager.ClientSendMessage(new MsgCommand() { Command = command }); + break; } + } } public void StartRepeatingCommand(string command) { - _netManager.ClientSendMessage(new MsgCommandRepeatStart(){Command = command}); + _netManager.ClientSendMessage(new MsgCommandRepeatStart() { Command = command }); } public void StopRepeatingCommand(string command) { - _netManager.ClientSendMessage(new MsgCommandRepeatStop(){Command = command}); + _netManager.ClientSendMessage(new MsgCommandRepeatStop() { Command = command }); } public void WinSet(string? controlId, string winsetParams) { @@ -615,7 +617,7 @@ public void WinClone(string controlId, string cloneId) { // that name already, we will create a new control of that type from scratch. if (elementDescriptor == null) { switch (controlId) { - case "window" : + case "window": elementDescriptor = new WindowDescriptor(cloneId); break; case "menu": @@ -636,7 +638,7 @@ public void WinClone(string controlId, string cloneId) { } LoadDescriptor(elementDescriptor); - if(elementDescriptor is WindowDescriptor && Windows.TryGetValue(cloneId, out var window)){ + if (elementDescriptor is WindowDescriptor && Windows.TryGetValue(cloneId, out var window)) { window.CreateChildControls(); } } diff --git a/OpenDreamClient/Interface/Prompts/ColorPrompt.cs b/OpenDreamClient/Interface/Prompts/ColorPrompt.cs new file mode 100644 index 0000000000..81dc53e04c --- /dev/null +++ b/OpenDreamClient/Interface/Prompts/ColorPrompt.cs @@ -0,0 +1,175 @@ +using System.Linq; +using Linguini.Bundle.Errors; +using OpenDreamShared.Dream; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.Stylesheets; + +namespace OpenDreamClient.Interface.Prompts; + +internal sealed class ColorPrompt : InputWindow { + private readonly BoxContainer _baseControl; + private readonly ColorSelectorSliders _colorSelector; + private readonly LineEdit _hexColor; + private readonly Button _preview; + private readonly Color _originalColor; + + public ColorPrompt(string title, string message, string defaultValue, bool canCancel, + Action? onClose, bool alpha = false) : base(title, message, canCancel, onClose) { + _originalColor = Color.FromHex(defaultValue, Color.White); + _colorSelector = new() { + Color = _originalColor, + VerticalAlignment = VAlignment.Top, + Stylesheet = ColorPromptStylesheet.Make(), + IsAlphaVisible = alpha, + OnColorChanged = ColorSelectorSliders_OnColorChanged + }; + var defaultHex = _colorSelector.IsAlphaVisible ? _originalColor.ToHex() : _originalColor.ToHexNoAlpha(); + _hexColor = new LineEdit { + Text = defaultHex, + HorizontalExpand = true, + PlaceHolder = _colorSelector.IsAlphaVisible ? "#RRGGBBAA" : "#RRGGBB", + IsValid = (string text) => { + text = text.Trim().TrimStart('#'); + return text.Length <= (_colorSelector.IsAlphaVisible ? 8 : 6) && text.All(char.IsAsciiHexDigit); + } + }; + _hexColor.OnFocusExit += LineEdit_OnFinishInput; + _hexColor.OnTextEntered += LineEdit_OnFinishInput; + + _preview = new Button { + SetSize = new Vector2(32, 32), + Modulate = _originalColor, + MouseFilter = MouseFilterMode.Ignore, + }; + + _baseControl = new BoxContainer { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Children = { + _colorSelector, + new BoxContainer { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + HorizontalExpand = true, + SeparationOverride = 8, + Children = { _preview, _hexColor } + } + } + }; + + SetPromptControl(_baseControl, grabKeyboard: false); + } + + private void ColorSelectorSliders_OnColorChanged(Color color) { + _hexColor.Text = _colorSelector.IsAlphaVisible ? color.ToHex() : color.ToHexNoAlpha(); + _preview.Modulate = color; + } + + private void LineEdit_OnFinishInput(LineEdit.LineEditEventArgs args) { + var text = args.Text.Trim(); + if (!text.StartsWith('#')) text = '#' + text; + var newColor = Color.TryFromHex(text); + if (newColor.HasValue) { + _colorSelector.Color = newColor.Value; + } + } + + protected override void OkButtonClicked() { + FinishPrompt(DMValueType.Color, _colorSelector.Color); + } +} + +// WHY IS ColorSelectorSliders A PART OF ROBUST, BUT THE STYLESHEET IT USES IS IN SS14?! +internal static class ColorPromptStylesheet { + private static readonly Color PanelDark = Color.FromHex("#1E1E22"); + private const string StyleClassSliderRed = "Red"; + private const string StyleClassSliderGreen = "Green"; + private const string StyleClassSliderBlue = "Blue"; + private const string StyleClassSliderWhite = "White"; + + private static StyleBoxTexture MakeSliderFill(Color color) { + return new() { + Texture = IoCManager.Resolve().GetResource("/Textures/Interface/Nano/slider_fill.svg.96dpi.png").Texture, + Modulate = color, + PatchMarginLeft = 12, + PatchMarginRight = 12, + PatchMarginTop = 12, + PatchMarginBottom = 12, + }; + } + + private static StyleBoxTexture MakeSliderGrab() { + return new() { + Texture = IoCManager.Resolve().GetResource("/Textures/Interface/Nano/slider_grabber.svg.96dpi.png").Texture, + PatchMarginLeft = 12, + PatchMarginRight = 12, + PatchMarginTop = 12, + PatchMarginBottom = 12, + }; + } + + private static StyleBoxTexture MakeSliderOutline(Color color) { + return new() { + Texture = IoCManager.Resolve().GetResource("/Textures/Interface/Nano/slider_outline.svg.96dpi.png").Texture, + Modulate = color, + PatchMarginLeft = 12, + PatchMarginRight = 12, + PatchMarginTop = 12, + PatchMarginBottom = 12, + }; + } + + + public static Stylesheet Make() { + var sliderFillBox = MakeSliderFill(Color.FromHex("#3E6C45")); + var sliderBackBox = MakeSliderOutline(PanelDark); + var sliderForeBox = MakeSliderOutline(Color.FromHex("#494949")); + var sliderGrabBox = MakeSliderGrab(); + + var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen }; + var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red }; + var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue }; + var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White }; + + var styles = new DefaultStylesheet(IoCManager.Resolve(), IoCManager.Resolve()).Stylesheet.Rules.ToList(); + var newStyles = new StyleRule[] { + // Slider + new StyleRule(SelectorElement.Type(typeof(Slider)), new [] + { + new StyleProperty(Slider.StylePropertyBackground, sliderBackBox), + new StyleProperty(Slider.StylePropertyForeground, sliderForeBox), + new StyleProperty(Slider.StylePropertyGrabber, sliderGrabBox), + new StyleProperty(Slider.StylePropertyFill, sliderFillBox), + }), + + new StyleRule(SelectorElement.Type(typeof(ColorableSlider)), new [] + { + new StyleProperty(ColorableSlider.StylePropertyFillWhite, sliderFillWhite), + new StyleProperty(ColorableSlider.StylePropertyBackgroundWhite, sliderFillWhite), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderRed}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillRed), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderGreen}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillGreen), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderBlue}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillBlue), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderWhite}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillWhite), + }) + }; + styles.AddRange(newStyles); + return new Stylesheet(styles); + } +} diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 8dcf8a6271..6bc43f2579 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -30,7 +30,8 @@ public sealed class DreamConnection { [ViewVariables] public ICommonSession? Session { get; private set; } [ViewVariables] public DreamObjectClient? Client { get; private set; } - [ViewVariables] public DreamObjectMob? Mob { + [ViewVariables] + public DreamObjectMob? Mob { get => _mob; set { if (_mob != value) { @@ -234,7 +235,7 @@ public void AddStatPanelLine(string name, string value, string? atomRef) { if (_outputStatPanel == null || !_statPanels.ContainsKey(_outputStatPanel)) SetOutputStatPanel("Stats"); - _statPanels[_outputStatPanel].Add( (name, value, atomRef) ); + _statPanels[_outputStatPanel].Add((name, value, atomRef)); } public void HandleMsgSelectStatPanel(MsgSelectStatPanel message) { @@ -251,6 +252,7 @@ public void HandleMsgPromptResponse(MsgPromptResponse message) { DMValueType.Null => DreamValue.Null, DMValueType.Text or DMValueType.Message => new DreamValue((string)message.Value), DMValueType.Num => new DreamValue((float)message.Value), + DMValueType.Color => new DreamValue(((Color)message.Value).ToHexNoAlpha()), _ => throw new Exception("Invalid prompt response '" + message.Type + "'") }; @@ -372,7 +374,7 @@ public void HandleCommand(string fullCommand) { DMValueType argumentType = verb.ArgumentTypes[i]; if (argumentType == DMValueType.Text) { - arguments[i] = new(args[i+1]); + arguments[i] = new(args[i + 1]); } else { _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); return DreamValue.Null; diff --git a/OpenDreamShared/Network/Messages/MsgPromptResponse.cs b/OpenDreamShared/Network/Messages/MsgPromptResponse.cs index edd12eeb13..8acbe4f254 100644 --- a/OpenDreamShared/Network/Messages/MsgPromptResponse.cs +++ b/OpenDreamShared/Network/Messages/MsgPromptResponse.cs @@ -1,6 +1,7 @@ using System; using Lidgren.Network; using OpenDreamShared.Dream; +using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -14,12 +15,13 @@ public sealed class MsgPromptResponse : NetMessage { public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { PromptId = buffer.ReadVariableInt32(); - Type = (DMValueType) buffer.ReadUInt16(); + Type = (DMValueType)buffer.ReadUInt16(); Value = Type switch { DMValueType.Null => null, DMValueType.Text or DMValueType.Message => buffer.ReadString(), DMValueType.Num => buffer.ReadSingle(), + DMValueType.Color => new Color(buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte()), _ => throw new ArgumentOutOfRangeException() }; } @@ -27,14 +29,21 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { buffer.WriteVariableInt32(PromptId); - buffer.Write((ushort) Type); + buffer.Write((ushort)Type); switch (Type) { case DMValueType.Null: break; case DMValueType.Text or DMValueType.Message: - buffer.Write((string) Value!); + buffer.Write((string)Value!); break; case DMValueType.Num: - buffer.Write((float) Value!); + buffer.Write((float)Value!); + break; + case DMValueType.Color: + var color = (Color)Value!; + buffer.Write(color.RByte); + buffer.Write(color.GByte); + buffer.Write(color.BByte); + buffer.Write(color.AByte); break; default: throw new Exception("Invalid prompt response type '" + Type + "'"); } diff --git a/Resources/Textures/Interface/Nano/slider_fill.svg b/Resources/Textures/Interface/Nano/slider_fill.svg new file mode 100644 index 0000000000..615a923d6b --- /dev/null +++ b/Resources/Textures/Interface/Nano/slider_fill.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Resources/Textures/Interface/Nano/slider_fill.svg.96dpi.png b/Resources/Textures/Interface/Nano/slider_fill.svg.96dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..05a780078f98dcb1cdee34c2c165698c9e31d691 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI23?!pd0{;Rj=3*z$5DpHG+YkL80J)q69+AZi z417mGm~pB$pEOWVvcxr_Bsf2(yEr+qAXP8FD1G)j8<44@0X`wF zK>9xh@Hn5#0+e7Y3GxeO(9nJM^3{6h-)TURFi#i95Q*@qCpYpoIPf?JDl3X5H2OH6 z<(aAAyXQZL$i=yDCf&EKJ+RAT(whK7U*qX%d8I36EaMZsd}ZOI>{aqszQ-3I%AFK> g0R}Cjinsh`5VKE+y;)!^1hj#{)78&qol`;+0DJ9Y`Tzg` literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Nano/slider_grabber.svg b/Resources/Textures/Interface/Nano/slider_grabber.svg new file mode 100644 index 0000000000..e7075cfb26 --- /dev/null +++ b/Resources/Textures/Interface/Nano/slider_grabber.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Resources/Textures/Interface/Nano/slider_grabber.svg.96dpi.png b/Resources/Textures/Interface/Nano/slider_grabber.svg.96dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..557d1ca819b3fe792405d8491ad150e49c84211a GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^5(#!^6wV3&=1qFtD(&u(Y%UvUwK;t^!K4mIV0)GnCgibZkDSv;ZiO?CIhd zA`#wuVJB~c1CL8!zIaPZ%M=G;Gf{ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Resources/Textures/Interface/Nano/slider_outline.svg.96dpi.png b/Resources/Textures/Interface/Nano/slider_outline.svg.96dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..de74c2dee04cecabc4b0e8afaa87d93d481a07bc GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI11|(N{`J4k%EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhK*rAWba4#v z=)8NykngYo53|F{e4Yt>f8FOkena-&Mag*7D+JdxF`^q&TwP7*$TWk$YsNftI(ZgzPQ$H6d?8Qr~b- Y`8L1({BIi(pd%SPUHx3vIVCg!0B=QNH2?qr literal 0 HcmV?d00001 From d86fa80d79181e65d05fe11e3629cb2609e3ef05 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:36:36 +0000 Subject: [PATCH 38/64] on-show and on-hide for info and browser, handle winset attribute=; (#1564) --- .../Interface/Controls/ControlBrowser.cs | 26 ++++++++++++++++++- .../Interface/Controls/ControlInfo.cs | 26 ++++++++++++++++++- OpenDreamClient/Interface/DMF/DMFParser.cs | 5 +++- .../Descriptors/ControlDescriptors.cs | 8 ++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/OpenDreamClient/Interface/Controls/ControlBrowser.cs b/OpenDreamClient/Interface/Controls/ControlBrowser.cs index 6c0eeb9933..85d6ad5ee4 100644 --- a/OpenDreamClient/Interface/Controls/ControlBrowser.cs +++ b/OpenDreamClient/Interface/Controls/ControlBrowser.cs @@ -46,7 +46,17 @@ protected override Control CreateUIElement() { _webView.AddResourceRequestHandler(RequestHandler); _webView.AddBeforeBrowseHandler(BeforeBrowseHandler); - + _webView.OnVisibilityChanged += (args) => { + if (args.Visible) { + OnShowEvent(); + } else { + OnHideEvent(); + } + }; + if(ControlDescriptor.IsVisible) + OnShowEvent(); + else + OnHideEvent(); return _webView; } @@ -146,6 +156,20 @@ private void HandleEmbeddedWinset(string query) { // We can finally call winset _interfaceManager.WinSet(element, modifiedQuery); } + + public void OnShowEvent() { + ControlDescriptorBrowser controlDescriptor = (ControlDescriptorBrowser)ControlDescriptor; + if (controlDescriptor.OnShowCommand != null) { + _interfaceManager.RunCommand(controlDescriptor.OnShowCommand); + } + } + + public void OnHideEvent() { + ControlDescriptorBrowser controlDescriptor = (ControlDescriptorBrowser)ControlDescriptor; + if (controlDescriptor.OnHideCommand != null) { + _interfaceManager.RunCommand(controlDescriptor.OnHideCommand); + } + } } public sealed class BrowseWinCommand : IConsoleCommand { diff --git a/OpenDreamClient/Interface/Controls/ControlInfo.cs b/OpenDreamClient/Interface/Controls/ControlInfo.cs index a6f3bce85b..884ee6e261 100644 --- a/OpenDreamClient/Interface/Controls/ControlInfo.cs +++ b/OpenDreamClient/Interface/Controls/ControlInfo.cs @@ -205,7 +205,17 @@ protected override Control CreateUIElement() { _tabControl.OnTabChanged += OnSelectionChanged; RefreshVerbs(); - + _tabControl.OnVisibilityChanged += (args) => { + if (args.Visible) { + OnShowEvent(); + } else { + OnHideEvent(); + } + }; + if(ControlDescriptor.IsVisible) + OnShowEvent(); + else + OnHideEvent(); return _tabControl; } @@ -285,4 +295,18 @@ private void OnSelectionChanged(int tabIndex) { _netManager.ClientSendMessage(msg); } + + public void OnShowEvent() { + ControlDescriptorInfo controlDescriptor = (ControlDescriptorInfo)ControlDescriptor; + if (controlDescriptor.OnShowCommand != null) { + _interfaceManager.RunCommand(controlDescriptor.OnShowCommand); + } + } + + public void OnHideEvent() { + ControlDescriptorInfo controlDescriptor = (ControlDescriptorInfo)ControlDescriptor; + if (controlDescriptor.OnHideCommand != null) { + _interfaceManager.RunCommand(controlDescriptor.OnHideCommand); + } + } } diff --git a/OpenDreamClient/Interface/DMF/DMFParser.cs b/OpenDreamClient/Interface/DMF/DMFParser.cs index 1f6be372b9..397edc8b53 100644 --- a/OpenDreamClient/Interface/DMF/DMFParser.cs +++ b/OpenDreamClient/Interface/DMF/DMFParser.cs @@ -215,7 +215,10 @@ private bool TryGetAttribute(out string? element, [NotNullWhen(true)] out string if (!Check(TokenType.DMF_Value) && !Check(TokenType.DMF_Attribute)) Error($"Invalid attribute value ({valueText})"); } else if (!Check(TokenType.DMF_Value)) - Error($"Invalid attribute value ({valueText})"); + if(Check(TokenType.DMF_Semicolon)) //thing.attribute=; means thing.attribute=empty string + valueText = ""; + else + Error($"Invalid attribute value ({valueText})"); Newline(); key = attributeToken.Text; diff --git a/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs b/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs index 8ab31110cf..9e9b17658d 100644 --- a/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs +++ b/OpenDreamClient/Interface/Descriptors/ControlDescriptors.cs @@ -136,6 +136,10 @@ public sealed partial class ControlDescriptorOutput : ControlDescriptor { } public sealed partial class ControlDescriptorInfo : ControlDescriptor { + [DataField("on-show")] + public string? OnShowCommand; + [DataField("on-hide")] + public string? OnHideCommand; [DataField("allow-html")] public bool AllowHtml = true; // Supposedly false by default, but it isn't if you're not using BYOND's default skin } @@ -150,6 +154,10 @@ public sealed partial class ControlDescriptorMap : ControlDescriptor { } public sealed partial class ControlDescriptorBrowser : ControlDescriptor { + [DataField("on-show")] + public string? OnShowCommand; + [DataField("on-hide")] + public string? OnHideCommand; } public sealed partial class ControlDescriptorLabel : ControlDescriptor { From 1515f0d47ba7b14cee02f75ab661fb5e4a1de5d7 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 3 Jan 2024 13:04:41 -0800 Subject: [PATCH 39/64] Add support for click-and-dragging atoms (#1602) * Remove `ClickMapManager` We haven't used these click maps in ages * File-scoped namespaces * Add support for click-dragging atoms * A file-scoped namespace * Remove some duplicated code --- DMCompiler/DMStandard/Types/Atoms/_Atom.dm | 1 - DMCompiler/DMStandard/Types/Client.dm | 1 - OpenDreamClient/ClientContentIoC.cs | 18 +- OpenDreamClient/Input/ClickMapManager.cs | 91 -------- OpenDreamClient/Input/MouseInputSystem.cs | 210 +++++++++++------- .../Interface/Controls/ControlMap.cs | 7 +- .../Resources/ResourceTypes/DMIResource.cs | 125 +++++------ OpenDreamRuntime/AtomManager.cs | 17 +- OpenDreamRuntime/Input/MouseInputSystem.cs | 123 +++++----- OpenDreamShared/Dream/AtomReference.cs | 33 +++ .../Input/SharedMouseInputSystem.cs | 93 ++------ 11 files changed, 350 insertions(+), 369 deletions(-) delete mode 100644 OpenDreamClient/Input/ClickMapManager.cs create mode 100644 OpenDreamShared/Dream/AtomReference.cs diff --git a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm index faee9c15d4..6e9b9b5cda 100644 --- a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm +++ b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm @@ -71,7 +71,6 @@ set opendream_unimplemented = TRUE proc/MouseDrop(over_object,src_location,over_location,src_control,over_control,params) - set opendream_unimplemented = TRUE proc/MouseEntered(location,control,params) set opendream_unimplemented = TRUE diff --git a/DMCompiler/DMStandard/Types/Client.dm b/DMCompiler/DMStandard/Types/Client.dm index 4f6f895580..e0ae143145 100644 --- a/DMCompiler/DMStandard/Types/Client.dm +++ b/DMCompiler/DMStandard/Types/Client.dm @@ -134,7 +134,6 @@ src_object.MouseDrag(over_object,src_location,over_location,src_control,over_control,params) proc/MouseDrop(atom/src_object,over_object,src_location,over_location,src_control,over_control,params) - set opendream_unimplemented = TRUE src_object.MouseDrop(over_object,src_location,over_location,src_control,over_control,params) proc/MouseEntered(atom/object,location,control,params) diff --git a/OpenDreamClient/ClientContentIoC.cs b/OpenDreamClient/ClientContentIoC.cs index 608d1c6535..7f4cd1c5d3 100644 --- a/OpenDreamClient/ClientContentIoC.cs +++ b/OpenDreamClient/ClientContentIoC.cs @@ -1,17 +1,15 @@ using OpenDreamClient.Audio; -using OpenDreamClient.Input; using OpenDreamClient.Interface; using OpenDreamClient.Resources; using OpenDreamClient.States; -namespace OpenDreamClient { - public static class ClientContentIoC { - public static void Register() { - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - IoCManager.Register(); - } +namespace OpenDreamClient; + +public static class ClientContentIoC { + public static void Register() { + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); } } diff --git a/OpenDreamClient/Input/ClickMapManager.cs b/OpenDreamClient/Input/ClickMapManager.cs deleted file mode 100644 index 9c7c1f24a1..0000000000 --- a/OpenDreamClient/Input/ClickMapManager.cs +++ /dev/null @@ -1,91 +0,0 @@ -using OpenDreamClient.Resources.ResourceTypes; -using Robust.Client.Graphics; -using Robust.Client.Utility; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace OpenDreamClient.Input { - //Lifted from SS14 for the most part - //Used for atoms with an opaque mouse_opacity - internal sealed class ClickMapManager : IClickMapManager { - [ViewVariables] - private readonly Dictionary _clickMaps = new(); - - public void CreateClickMap(DMIResource.State state, Image image) { - foreach (AtlasTexture[] textures in state.Frames.Values) { - foreach (AtlasTexture texture in textures) { - _clickMaps[texture] = ClickMap.FromImage(image, texture); - } - } - } - - public bool IsOccluding(AtlasTexture texture, Vector2i pos) { - if (!_clickMaps.TryGetValue(texture, out var clickMap)) { - return false; - } - - return SampleClickMap(clickMap, pos, clickMap.Size, Vector2i.Zero); - } - - private static bool SampleClickMap(ClickMap map, Vector2i pos, Vector2i bounds, Vector2i offset) { - var (width, height) = bounds; - var (px, py) = pos; - - if (px < 0 || px >= width || py < 0 || py >= height) - return false; - - return map.IsOccluded(px, py); - } - - internal sealed class ClickMap { - [ViewVariables] private readonly byte[] _data; - - public int Width { get; } - public int Height { get; } - [ViewVariables] public Vector2i Size => (Width, Height); - - public bool IsOccluded(int x, int y) { - var i = y * Width + x; - return (_data[i / 8] & (1 << (i % 8))) != 0; - } - - public bool IsOccluded(Vector2i vector) { - var (x, y) = vector; - return IsOccluded(x, y); - } - - private ClickMap(byte[] data, int width, int height) { - Width = width; - Height = height; - _data = data; - } - - public static ClickMap FromImage(Image image, AtlasTexture atlas) where T : unmanaged, IPixel { - var width = (int)atlas.SubRegion.Width; - var height = (int)atlas.SubRegion.Height; - - var dataSize = (int)Math.Ceiling(width * height / 8f); - var data = new byte[dataSize]; - - var pixelSpan = image.GetPixelSpan(); - - for (var i = 0; i < width*height; i++) { - var y = (int)atlas.SubRegion.Top + (i / width); - var x = (int)atlas.SubRegion.Left + (i % width); - Rgba32 rgba = default; - pixelSpan[y * image.Width + x].ToRgba32(ref rgba); - if (rgba.A > 0) { - data[i / 8] |= (byte)(1 << (i % 8)); - } - } - - return new ClickMap(data, width, height); - } - } - } - - public interface IClickMapManager { - public void CreateClickMap(DMIResource.State state, Image image); - public bool IsOccluding(AtlasTexture texture, Vector2i pos); - } -} diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 4a77a106c1..cf512dbb28 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -4,6 +4,7 @@ using OpenDreamClient.Rendering; using OpenDreamShared.Dream; using OpenDreamShared.Input; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.UserInterface; @@ -11,106 +12,163 @@ using Robust.Shared.Input.Binding; using Robust.Shared.Map; -namespace OpenDreamClient.Input { - internal sealed class MouseInputSystem : SharedMouseInputSystem { - [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IOverlayManager _overlayManager = default!; - [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - private DreamViewOverlay? _dreamViewOverlay; - private ContextMenuPopup _contextMenu = default!; - - public override void Initialize() { - _contextMenu = new ContextMenuPopup(); - _userInterfaceManager.ModalRoot.AddChild(_contextMenu); - } +namespace OpenDreamClient.Input; + +internal sealed class MouseInputSystem : SharedMouseInputSystem { + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly MapSystem _mapSystem = default!; + + private DreamViewOverlay? _dreamViewOverlay; + private ContextMenuPopup _contextMenu = default!; + private EntityClickInformation? _selectedEntity; + + private sealed class EntityClickInformation(AtomReference atom, ScreenCoordinates initialMousePos, ClickParams clickParams) { + public readonly AtomReference Atom = atom; + public readonly ScreenCoordinates InitialMousePos = initialMousePos; + public readonly ClickParams ClickParams = clickParams; + public bool IsDrag; // If the current click is considered a drag (if the mouse has moved after the click) + } + + public override void Initialize() { + UpdatesOutsidePrediction = true; + + _contextMenu = new ContextMenuPopup(); + _userInterfaceManager.ModalRoot.AddChild(_contextMenu); + } + + public override void Update(float frameTime) { + if (_selectedEntity == null) + return; - public override void Shutdown() { - CommandBinds.Unregister(); + if (!_selectedEntity.IsDrag) { + var currentMousePos = _inputManager.MouseScreenPosition.Position; + var distance = (currentMousePos - _selectedEntity.InitialMousePos.Position).Length(); + + if (distance > 3f) + _selectedEntity.IsDrag = true; } + } + + public override void Shutdown() { + CommandBinds.Unregister(); + } - public bool HandleViewportClick(ScalingViewport viewport, GUIBoundKeyEventArgs args) { - UIBox2i viewportBox = viewport.GetDrawBox(); - if (!viewportBox.Contains((int)args.RelativePixelPosition.X, (int)args.RelativePixelPosition.Y)) - return false; // Click was outside of the viewport + public bool HandleViewportEvent(ScalingViewport viewport, GUIBoundKeyEventArgs args) { + if (args.State == BoundKeyState.Down) + return OnPress(viewport, args); + else + return OnRelease(viewport, args); + } - bool middle = args.Function == OpenDreamKeyFunctions.MouseMiddle; - bool shift = _inputManager.IsKeyDown(Keyboard.Key.Shift); - bool ctrl = _inputManager.IsKeyDown(Keyboard.Key.Control); - bool alt = _inputManager.IsKeyDown(Keyboard.Key.Alt); + public void HandleStatClick(string atomRef, bool isMiddle) { + bool shift = _inputManager.IsKeyDown(Keyboard.Key.Shift); + bool ctrl = _inputManager.IsKeyDown(Keyboard.Key.Control); + bool alt = _inputManager.IsKeyDown(Keyboard.Key.Alt); - Vector2 screenLocPos = (args.RelativePixelPosition - viewportBox.TopLeft) / viewportBox.Size * viewport.ViewportSize; + RaiseNetworkEvent(new StatClickedEvent(atomRef, isMiddle, shift, ctrl, alt)); + } - var screenLocY = viewport.ViewportSize.Y - screenLocPos.Y; // Flip the Y - ScreenLocation screenLoc = new ScreenLocation((int) screenLocPos.X, (int) screenLocY, 32); // TODO: icon_size other than 32 + private (AtomReference Atom, Vector2i IconPosition)? GetAtomUnderMouse(ScalingViewport viewport, GUIBoundKeyEventArgs args) { + _dreamViewOverlay ??= _overlayManager.GetOverlay(); + if(_dreamViewOverlay.MouseMap == null) + return null; - MapCoordinates mapCoords = viewport.ScreenToMap(args.PointerLocation.Position); - RendererMetaData? entity = GetEntityUnderMouse(screenLocPos); - if (entity == null) - return false; + UIBox2i viewportBox = viewport.GetDrawBox(); + if (!viewportBox.Contains((int)args.RelativePixelPosition.X, (int)args.RelativePixelPosition.Y)) + return null; // Was outside of the viewport - if (entity.ClickUid == EntityUid.Invalid && args.Function != EngineKeyFunctions.UIRightClick) { // Turf was clicked and not a right-click - // Grid coordinates are half a meter off from entity coordinates - mapCoords = new MapCoordinates(mapCoords.Position + new Vector2(0.5f), mapCoords.MapId); + var mapCoords = viewport.ScreenToMap(args.PointerLocation.Position); + var mousePos = (args.RelativePixelPosition - viewportBox.TopLeft) / viewportBox.Size * viewport.ViewportSize; + var lookupColor = _dreamViewOverlay.MouseMap.GetPixel((int)mousePos.X, (int)mousePos.Y); + var underMouse = _dreamViewOverlay.MouseMapLookup.GetValueOrDefault(lookupColor); + if (underMouse == null) + return null; - if (_mapManager.TryFindGridAt(mapCoords, out _, out var grid)){ - Vector2i position = grid.CoordinatesToTile(grid.MapToGrid(mapCoords)); - MapCoordinates worldPosition = grid.GridTileToWorld(position); - Vector2i turfIconPosition = (Vector2i) ((mapCoords.Position - position) * EyeManager.PixelsPerMeter); - RaiseNetworkEvent(new TurfClickedEvent(position, (int)worldPosition.MapId, screenLoc, middle, shift, ctrl, alt, turfIconPosition)); - } + if (underMouse.ClickUid == EntityUid.Invalid) { // A turf + // Grid coordinates are half a meter off from entity coordinates + mapCoords = new MapCoordinates(mapCoords.Position + new Vector2(0.5f), mapCoords.MapId); - return true; + if (_mapManager.TryFindGridAt(mapCoords, out var gridEntity, out var grid)) { + Vector2i position = _mapSystem.CoordinatesToTile(gridEntity, grid, _mapSystem.MapToGrid(gridEntity, mapCoords)); + Vector2i turfIconPosition = (Vector2i) ((mapCoords.Position - position) * EyeManager.PixelsPerMeter); + MapCoordinates worldPosition = _mapSystem.GridTileToWorld(gridEntity, grid, position); + + return (new(position, (int)worldPosition.MapId), turfIconPosition); } - if (args.Function == EngineKeyFunctions.UIRightClick) { //either turf or atom was clicked, and it was a right-click - var entities = _lookupSystem.GetEntitiesInRange(mapCoords, 0.01f); + return null; + } else { + Vector2i iconPosition = (Vector2i) ((mapCoords.Position - underMouse.Position) * EyeManager.PixelsPerMeter); + + return (new(_entityManager.GetNetEntity(underMouse.ClickUid)), iconPosition); + } + } - //TODO filter entities by the valid verbs that exist on them - //they should only show up if there is a verb attached to usr which matches the filter in world syntax - //ie, obj|turf in world - //note that popup_menu = 0 overrides this behaviour, as does verb invisibility (urgh), and also hidden - //because BYOND sure loves redundancy + private bool OnPress(ScalingViewport viewport, GUIBoundKeyEventArgs args) { + if (args.Function == EngineKeyFunctions.UIRightClick) { //either turf or atom was clicked, and it was a right-click + var mapCoords = viewport.ScreenToMap(args.PointerLocation.Position); + var entities = _lookupSystem.GetEntitiesInRange(mapCoords, 0.01f); - _contextMenu.RepopulateEntities(entities); - if(_contextMenu.EntityCount == 0) - return true; //don't open a 1x1 empty context menu + //TODO filter entities by the valid verbs that exist on them + //they should only show up if there is a verb attached to usr which matches the filter in world syntax + //ie, obj|turf in world + //note that popup_menu = 0 overrides this behaviour, as does verb invisibility (urgh), and also hidden + //because BYOND sure loves redundancy - _contextMenu.Measure(_userInterfaceManager.ModalRoot.Size); - Vector2 contextMenuLocation = args.PointerLocation.Position / _userInterfaceManager.ModalRoot.UIScale; // Take scaling into account - _contextMenu.Open(UIBox2.FromDimensions(contextMenuLocation, _contextMenu.DesiredSize)); + _contextMenu.RepopulateEntities(entities); + if(_contextMenu.EntityCount == 0) + return true; //don't open a 1x1 empty context menu - return true; - } + _contextMenu.Measure(_userInterfaceManager.ModalRoot.Size); + Vector2 contextMenuLocation = args.PointerLocation.Position / _userInterfaceManager.ModalRoot.UIScale; // Take scaling into account + _contextMenu.Open(UIBox2.FromDimensions(contextMenuLocation, _contextMenu.DesiredSize)); - // TODO: Take icon transformations into account - Vector2i iconPosition = (Vector2i) ((mapCoords.Position - entity.Position) * EyeManager.PixelsPerMeter); - NetEntity ent = _entityManager.GetNetEntity(entity.ClickUid); - RaiseNetworkEvent(new EntityClickedEvent(ent, screenLoc, middle, shift, ctrl, alt, iconPosition)); return true; } - public void HandleStatClick(string atomRef, bool isMiddle) { - bool shift = _inputManager.IsKeyDown(Keyboard.Key.Shift); - bool ctrl = _inputManager.IsKeyDown(Keyboard.Key.Control); - bool alt = _inputManager.IsKeyDown(Keyboard.Key.Alt); + var underMouse = GetAtomUnderMouse(viewport, args); + if (underMouse == null) + return false; - RaiseNetworkEvent(new StatClickedEvent(atomRef, isMiddle, shift, ctrl, alt)); - } + var atom = underMouse.Value.Atom; + var clickParams = CreateClickParams(viewport, args, underMouse.Value.IconPosition); - private RendererMetaData? GetEntityUnderMouse(Vector2 mousePos) { - _dreamViewOverlay ??= _overlayManager.GetOverlay(); - if(_dreamViewOverlay.MouseMap == null) - return null; + _selectedEntity = new(atom, args.PointerLocation, clickParams); + return true; + } + + private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { + if (_selectedEntity == null) + return false; - Color lookupColor = _dreamViewOverlay.MouseMap.GetPixel((int)mousePos.X, (int)mousePos.Y); - if(!_dreamViewOverlay.MouseMapLookup.TryGetValue(lookupColor, out var result)) - return null; + if (!_selectedEntity.IsDrag) { + RaiseNetworkEvent(new AtomClickedEvent(_selectedEntity.Atom, _selectedEntity.ClickParams)); + } else { + var overAtom = GetAtomUnderMouse(viewport, args); - return result; + RaiseNetworkEvent(new AtomDraggedEvent(_selectedEntity.Atom, overAtom?.Atom, _selectedEntity.ClickParams)); } + + _selectedEntity = null; + return true; + } + + private ClickParams CreateClickParams(ScalingViewport viewport, GUIBoundKeyEventArgs args, Vector2i iconPos) { + bool middle = args.Function == OpenDreamKeyFunctions.MouseMiddle; + bool shift = _inputManager.IsKeyDown(Keyboard.Key.Shift); + bool ctrl = _inputManager.IsKeyDown(Keyboard.Key.Control); + bool alt = _inputManager.IsKeyDown(Keyboard.Key.Alt); + UIBox2i viewportBox = viewport.GetDrawBox(); + Vector2 screenLocPos = (args.RelativePixelPosition - viewportBox.TopLeft) / viewportBox.Size * viewport.ViewportSize; + float screenLocY = viewport.ViewportSize.Y - screenLocPos.Y; // Flip the Y + ScreenLocation screenLoc = new ScreenLocation((int) screenLocPos.X, (int) screenLocY, 32); // TODO: icon_size other than 32 + + // TODO: Take icon transformations into account for iconPos + return new(screenLoc, middle, shift, ctrl, alt, iconPos.X, iconPos.Y); } } diff --git a/OpenDreamClient/Interface/Controls/ControlMap.cs b/OpenDreamClient/Interface/Controls/ControlMap.cs index 75e2689fbf..be81d59ede 100644 --- a/OpenDreamClient/Interface/Controls/ControlMap.cs +++ b/OpenDreamClient/Interface/Controls/ControlMap.cs @@ -36,7 +36,8 @@ public void UpdateViewRange(ViewRange view) { protected override Control CreateUIElement() { Viewport = new ScalingViewport { MouseFilter = Control.MouseFilterMode.Stop }; - Viewport.OnKeyBindDown += OnViewportKeyBindDown; + Viewport.OnKeyBindDown += OnViewportKeyBindEvent; + Viewport.OnKeyBindUp += OnViewportKeyBindEvent; Viewport.OnVisibilityChanged += (args) => { if (args.Visible) { OnShowEvent(); @@ -54,12 +55,12 @@ protected override Control CreateUIElement() { return new PanelContainer { StyleClasses = {"MapBackground"}, Children = { Viewport } }; } - private void OnViewportKeyBindDown(GUIBoundKeyEventArgs e) { + private void OnViewportKeyBindEvent(GUIBoundKeyEventArgs e) { if (e.Function == EngineKeyFunctions.Use || e.Function == EngineKeyFunctions.TextCursorSelect || e.Function == EngineKeyFunctions.UIRightClick || e.Function == OpenDreamKeyFunctions.MouseMiddle) { _entitySystemManager.Resolve(ref _mouseInput); - if (_mouseInput.HandleViewportClick(Viewport, e)) { + if (_mouseInput.HandleViewportEvent(Viewport, e)) { e.Handle(); } } diff --git a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs index 0df29d3728..c40fb20a2a 100644 --- a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs +++ b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs @@ -1,100 +1,95 @@ using System.IO; -using OpenDreamClient.Input; using OpenDreamShared.Dream; using OpenDreamShared.Resources; using Robust.Client.Graphics; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -namespace OpenDreamClient.Resources.ResourceTypes { - public sealed class DMIResource : DreamResource { - private readonly byte[] _pngHeader = { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA }; +namespace OpenDreamClient.Resources.ResourceTypes; - public Texture Texture; - public Vector2i IconSize; - public DMIParser.ParsedDMIDescription Description; +public sealed class DMIResource : DreamResource { + private readonly byte[] _pngHeader = { 0x89, 0x50, 0x4E, 0x47, 0xD, 0xA, 0x1A, 0xA }; - private Dictionary _states; + public Texture Texture; + public Vector2i IconSize; + public DMIParser.ParsedDMIDescription Description; - public DMIResource(int id, byte[] data) : base(id, data) { - if (!IsValidPNG()) throw new Exception("Attempted to create a DMI using an invalid PNG"); + private readonly Dictionary _states; - using Stream dmiStream = new MemoryStream(data); - DMIParser.ParsedDMIDescription description = DMIParser.ParseDMI(dmiStream); + public DMIResource(int id, byte[] data) : base(id, data) { + if (!IsValidPNG()) throw new Exception("Attempted to create a DMI using an invalid PNG"); - dmiStream.Seek(0, SeekOrigin.Begin); + using Stream dmiStream = new MemoryStream(data); + DMIParser.ParsedDMIDescription description = DMIParser.ParseDMI(dmiStream); - Image image = Image.Load(dmiStream); - Texture = IoCManager.Resolve().LoadTextureFromImage(image); - IconSize = new Vector2i(description.Width, description.Height); - Description = description; + dmiStream.Seek(0, SeekOrigin.Begin); - IClickMapManager clickMapManager = IoCManager.Resolve(); + Image image = Image.Load(dmiStream); + Texture = IoCManager.Resolve().LoadTextureFromImage(image); + IconSize = new Vector2i(description.Width, description.Height); + Description = description; - _states = new Dictionary(); - foreach (DMIParser.ParsedDMIState parsedState in description.States.Values) { - State state = new State(Texture, parsedState, description.Width, description.Height); + _states = new Dictionary(); + foreach (DMIParser.ParsedDMIState parsedState in description.States.Values) { + State state = new State(Texture, parsedState, description.Width, description.Height); - _states.Add(parsedState.Name, state); - clickMapManager.CreateClickMap(state, image); - } + _states.Add(parsedState.Name, state); } + } - public State? GetState(string? stateName) { - if (stateName == null || !_states.ContainsKey(stateName)) - return _states.TryGetValue(String.Empty, out var state) ? state : null; // Default state, if one exists - - return _states[stateName]; - } + public State? GetState(string? stateName) { + if (stateName == null || !_states.ContainsKey(stateName)) + return _states.TryGetValue(string.Empty, out var state) ? state : null; // Default state, if one exists - private bool IsValidPNG() { - if (Data.Length < _pngHeader.Length) return false; + return _states[stateName]; + } - for (int i=0; i<_pngHeader.Length; i++) { - if (Data[i] != _pngHeader[i]) return false; - } + private bool IsValidPNG() { + if (Data.Length < _pngHeader.Length) return false; - return true; + for (int i=0; i<_pngHeader.Length; i++) { + if (Data[i] != _pngHeader[i]) return false; } - public struct State { - public Dictionary Frames; - - public State(Texture texture, DMIParser.ParsedDMIState parsedState, int width, int height) { - Frames = new Dictionary(); + return true; + } - foreach (KeyValuePair pair in parsedState.Directions) { - AtomDirection dir = pair.Key; - DMIParser.ParsedDMIFrame[] parsedFrames = pair.Value; - AtlasTexture[] frames = new AtlasTexture[parsedFrames.Length]; + public struct State { + public Dictionary Frames; - for (int i = 0; i < parsedFrames.Length; i++) { - DMIParser.ParsedDMIFrame parsedFrame = parsedFrames[i]; + public State(Texture texture, DMIParser.ParsedDMIState parsedState, int width, int height) { + Frames = new Dictionary(); - frames[i] = new AtlasTexture(texture, new UIBox2(parsedFrame.X, parsedFrame.Y, parsedFrame.X + width, parsedFrame.Y + height)); - } + foreach (KeyValuePair pair in parsedState.Directions) { + AtomDirection dir = pair.Key; + DMIParser.ParsedDMIFrame[] parsedFrames = pair.Value; + AtlasTexture[] frames = new AtlasTexture[parsedFrames.Length]; - Frames.Add(dir, frames); - } - } + for (int i = 0; i < parsedFrames.Length; i++) { + DMIParser.ParsedDMIFrame parsedFrame = parsedFrames[i]; - public AtlasTexture[] GetFrames(AtomDirection direction) { - // Find another direction to use if this one doesn't exist - if (!Frames.ContainsKey(direction)) { - // The diagonal directions attempt to use east/west - if (direction is AtomDirection.Northeast or AtomDirection.Southeast) - direction = AtomDirection.East; - else if (direction is AtomDirection.Northwest or AtomDirection.Southwest) - direction = AtomDirection.West; - - // Use the south direction if the above still isn't valid - if (!Frames.ContainsKey(direction)) - direction = AtomDirection.South; + frames[i] = new AtlasTexture(texture, new UIBox2(parsedFrame.X, parsedFrame.Y, parsedFrame.X + width, parsedFrame.Y + height)); } - return Frames[direction]; + Frames.Add(dir, frames); } } + public AtlasTexture[] GetFrames(AtomDirection direction) { + // Find another direction to use if this one doesn't exist + if (!Frames.ContainsKey(direction)) { + // The diagonal directions attempt to use east/west + if (direction is AtomDirection.Northeast or AtomDirection.Southeast) + direction = AtomDirection.East; + else if (direction is AtomDirection.Northwest or AtomDirection.Southwest) + direction = AtomDirection.West; + + // Use the south direction if the above still isn't valid + if (!Frames.ContainsKey(direction)) + direction = AtomDirection.South; + } + + return Frames[direction]; + } } } diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index 61e3230fd2..cdc5d03c2a 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -29,7 +29,7 @@ public sealed class AtomManager { private int _nextEmptyAreaSlot; private int _nextEmptyTurfSlot; - private readonly Dictionary _entityToAtom = new(); + private readonly Dictionary _entityToAtom = new(); private readonly Dictionary _definitionAppearanceCache = new(); private ServerAppearanceSystem AppearanceSystem => _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); @@ -196,7 +196,7 @@ public EntityUid CreateMovableEntity(DreamObjectMovable movable) { return entity; } - public bool TryGetMovableFromEntity(EntityUid entity, [NotNullWhen(true)] out DreamObject? movable) { + public bool TryGetMovableFromEntity(EntityUid entity, [NotNullWhen(true)] out DreamObjectMovable? movable) { return _entityToAtom.TryGetValue(entity, out movable); } @@ -205,6 +205,19 @@ public void DeleteMovableEntity(DreamObjectMovable movable) { _entityManager.DeleteEntity(movable.Entity); } + public DreamObjectAtom? GetAtom(AtomReference reference) { + switch (reference.AtomType) { + case AtomReference.RefType.Entity: + TryGetMovableFromEntity(_entityManager.GetEntity(reference.Entity), out var atom); + return atom; + case AtomReference.RefType.Turf: + _dreamMapManager.TryGetTurfAt((reference.TurfX, reference.TurfY), reference.TurfZ, out var turf); + return turf; + } + + return null; + } + public bool IsValidAppearanceVar(string name) { switch (name) { case "icon": diff --git a/OpenDreamRuntime/Input/MouseInputSystem.cs b/OpenDreamRuntime/Input/MouseInputSystem.cs index d2fd0603fc..3500141670 100644 --- a/OpenDreamRuntime/Input/MouseInputSystem.cs +++ b/OpenDreamRuntime/Input/MouseInputSystem.cs @@ -3,69 +3,90 @@ using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamShared.Input; -using Robust.Shared.Player; -namespace OpenDreamRuntime.Input { - internal sealed class MouseInputSystem : SharedMouseInputSystem { - [Dependency] private readonly AtomManager _atomManager = default!; - [Dependency] private readonly DreamManager _dreamManager = default!; - [Dependency] private readonly IDreamMapManager _dreamMapManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; +namespace OpenDreamRuntime.Input; - public override void Initialize() { - base.Initialize(); +internal sealed class MouseInputSystem : SharedMouseInputSystem { + [Dependency] private readonly AtomManager _atomManager = default!; + [Dependency] private readonly DreamManager _dreamManager = default!; + [Dependency] private readonly IDreamMapManager _mapManager = default!; - SubscribeNetworkEvent(OnEntityClicked); - SubscribeNetworkEvent(OnTurfClicked); - SubscribeNetworkEvent(OnStatClicked); - } + public override void Initialize() { + base.Initialize(); - private void OnEntityClicked(EntityClickedEvent e, EntitySessionEventArgs sessionEvent) { - EntityUid ent = _entityManager.GetEntity(e.NetEntity); - if (!_atomManager.TryGetMovableFromEntity(ent, out var atom)) - return; + SubscribeNetworkEvent(OnAtomClicked); + SubscribeNetworkEvent(OnAtomDragged); + SubscribeNetworkEvent(OnStatClicked); + } - HandleAtomClick(e, atom, sessionEvent); - } + private void OnAtomClicked(AtomClickedEvent e, EntitySessionEventArgs sessionEvent) { + var atom = _atomManager.GetAtom(e.Atom); + if (atom == null) + return; - private void OnTurfClicked(TurfClickedEvent e, EntitySessionEventArgs sessionEvent) { - if (!_dreamMapManager.TryGetTurfAt(e.Position, e.Z, out var turf)) - return; + HandleAtomClick(e, atom, sessionEvent); + } - HandleAtomClick(e, turf, sessionEvent); - } + private void OnAtomDragged(AtomDraggedEvent e, EntitySessionEventArgs sessionEvent) { + var src = _atomManager.GetAtom(e.SrcAtom); + if (src == null) + return; - private void OnStatClicked(StatClickedEvent e, EntitySessionEventArgs sessionEvent) { - if (!_dreamManager.LocateRef(e.AtomRef).TryGetValueAsDreamObject(out var dreamObject)) - return; + var over = (e.OverAtom != null) ? _atomManager.GetAtom(e.OverAtom.Value) : null; + var session = sessionEvent.SenderSession; + var connection = _dreamManager.GetConnectionBySession(session); + var usr = connection.Mob; + var srcPos = _atomManager.GetAtomPosition(src); - HandleAtomClick(e, dreamObject, sessionEvent); - } + _mapManager.TryGetTurfAt((srcPos.X, srcPos.Y), srcPos.Z, out var srcLoc); - private void HandleAtomClick(IAtomClickedEvent e, DreamObject atom, EntitySessionEventArgs sessionEvent) { - ICommonSession session = sessionEvent.SenderSession; - var connection = _dreamManager.GetConnectionBySession(session); - var usr = connection.Mob; + DreamValue overLocValue = DreamValue.Null; + if (over != null) { + var overPos = _atomManager.GetAtomPosition(over); - connection.Client?.SpawnProc("Click", usr: usr, ConstructClickArguments(atom, e)); + _mapManager.TryGetTurfAt((overPos.X, overPos.Y), overPos.Z, out var overLoc); + overLocValue = new(overLoc); } - private DreamValue[] ConstructClickArguments(DreamObject atom, IAtomClickedEvent e) { - NameValueCollection paramsBuilder = HttpUtility.ParseQueryString(String.Empty); - if (e.Middle) paramsBuilder.Add("middle", "1"); - if (e.Shift) paramsBuilder.Add("shift", "1"); - if (e.Ctrl) paramsBuilder.Add("ctrl", "1"); - if (e.Alt) paramsBuilder.Add("alt", "1"); - paramsBuilder.Add("screen-loc", e.ScreenLoc.ToString()); - paramsBuilder.Add("icon-x", e.IconX.ToString()); - paramsBuilder.Add("icon-y", e.IconY.ToString()); - - return new[] { - new DreamValue(atom), - DreamValue.Null, - DreamValue.Null, - new DreamValue(paramsBuilder.ToString()) - }; - } + connection.Client?.SpawnProc("MouseDrop", usr: usr, + new DreamValue(src), + new DreamValue(over), + new DreamValue(srcLoc), // TODO: Location can be a skin element + overLocValue, + DreamValue.Null, // TODO: src_control and over_control + DreamValue.Null, + new DreamValue(ConstructClickParams(e.Params))); + } + + private void OnStatClicked(StatClickedEvent e, EntitySessionEventArgs sessionEvent) { + if (!_dreamManager.LocateRef(e.AtomRef).TryGetValueAsDreamObject(out var dreamObject)) + return; + + HandleAtomClick(e, dreamObject, sessionEvent); + } + + private void HandleAtomClick(IAtomMouseEvent e, DreamObject atom, EntitySessionEventArgs sessionEvent) { + var session = sessionEvent.SenderSession; + var connection = _dreamManager.GetConnectionBySession(session); + var usr = connection.Mob; + + connection.Client?.SpawnProc("Click", usr: usr, + new DreamValue(atom), + DreamValue.Null, + DreamValue.Null, + new DreamValue(ConstructClickParams(e.Params))); + } + + private string ConstructClickParams(ClickParams clickParams) { + NameValueCollection paramsBuilder = HttpUtility.ParseQueryString(string.Empty); + if (clickParams.Middle) paramsBuilder.Add("middle", "1"); + if (clickParams.Shift) paramsBuilder.Add("shift", "1"); + if (clickParams.Ctrl) paramsBuilder.Add("ctrl", "1"); + if (clickParams.Alt) paramsBuilder.Add("alt", "1"); + paramsBuilder.Add("screen-loc", clickParams.ScreenLoc.ToString()); + paramsBuilder.Add("icon-x", clickParams.IconX.ToString()); + paramsBuilder.Add("icon-y", clickParams.IconY.ToString()); + + return paramsBuilder.ToString(); } } diff --git a/OpenDreamShared/Dream/AtomReference.cs b/OpenDreamShared/Dream/AtomReference.cs new file mode 100644 index 0000000000..624b65650d --- /dev/null +++ b/OpenDreamShared/Dream/AtomReference.cs @@ -0,0 +1,33 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; + +namespace OpenDreamShared.Dream; + +/// +/// Used by the client to refer to something that could be either a turf or an entity +/// +/// This should only be used on the client or when communicating with the client +[Serializable, NetSerializable] +public struct AtomReference { + public enum RefType { + Turf, + Entity + } + + public RefType AtomType; + public NetEntity Entity; + public int TurfX, TurfY, TurfZ; + + public AtomReference(Vector2i turfPos, int z) { + AtomType = RefType.Turf; + (TurfX, TurfY) = turfPos; + TurfZ = z; + } + + public AtomReference(NetEntity entity) { + AtomType = RefType.Entity; + Entity = entity; + } +} diff --git a/OpenDreamShared/Input/SharedMouseInputSystem.cs b/OpenDreamShared/Input/SharedMouseInputSystem.cs index 2d61eeda9b..0c7a78de12 100644 --- a/OpenDreamShared/Input/SharedMouseInputSystem.cs +++ b/OpenDreamShared/Input/SharedMouseInputSystem.cs @@ -3,91 +3,46 @@ using Robust.Shared.Serialization; using System; using OpenDreamShared.Dream; -using Robust.Shared.Maths; namespace OpenDreamShared.Input; [Virtual] public class SharedMouseInputSystem : EntitySystem { - protected interface IAtomClickedEvent { - public ScreenLocation ScreenLoc { get; } - public bool Middle { get; } - public bool Shift { get; } - public bool Ctrl { get; } - public bool Alt { get; } - public int IconX { get; } - public int IconY { get; } + protected interface IAtomMouseEvent { + public ClickParams Params { get; } } [Serializable, NetSerializable] - public sealed class EntityClickedEvent : EntityEventArgs, IAtomClickedEvent { - public NetEntity NetEntity { get; } - public ScreenLocation ScreenLoc { get; } - public bool Middle { get; } - public bool Shift { get; } - public bool Ctrl { get; } - public bool Alt { get; } - public int IconX { get; } - public int IconY { get; } - - public EntityClickedEvent(NetEntity netEntity, ScreenLocation screenLoc, bool middle, bool shift, bool ctrl, bool alt, Vector2i iconPos) { - NetEntity = netEntity; - ScreenLoc = screenLoc; - Middle = middle; - Shift = shift; - Ctrl = ctrl; - Alt = alt; - IconX = iconPos.X; - IconY = iconPos.Y; - } + public struct ClickParams(ScreenLocation screenLoc, bool middle, bool shift, bool ctrl, bool alt, int iconX, int iconY) { + public ScreenLocation ScreenLoc { get; } = screenLoc; + public bool Middle { get; } = middle; + public bool Shift { get; } = shift; + public bool Ctrl { get; } = ctrl; + public bool Alt { get; } = alt; + public int IconX { get; } = iconX; + public int IconY { get; } = iconY; } [Serializable, NetSerializable] - public sealed class TurfClickedEvent : EntityEventArgs, IAtomClickedEvent { - public Vector2i Position; - public ScreenLocation ScreenLoc { get; } - public int Z; - public bool Middle { get; } - public bool Shift { get; } - public bool Ctrl { get; } - public bool Alt { get; } - public int IconX { get; } - public int IconY { get; } + public sealed class AtomClickedEvent(AtomReference atom, ClickParams clickParams) : EntityEventArgs, IAtomMouseEvent { + public AtomReference Atom { get; } = atom; + public ClickParams Params { get; } = clickParams; + } - public TurfClickedEvent(Vector2i position, int z, ScreenLocation screenLoc, bool middle, bool shift, bool ctrl, bool alt, Vector2i iconPos) { - Position = position; - Z = z; - ScreenLoc = screenLoc; - Middle = middle; - Shift = shift; - Ctrl = ctrl; - Alt = alt; - IconX = iconPos.X; - IconY = iconPos.Y; - } + [Serializable, NetSerializable] + public sealed class AtomDraggedEvent(AtomReference src, AtomReference? over, ClickParams clickParams) : EntityEventArgs, IAtomMouseEvent { + public AtomReference SrcAtom { get; } = src; + public AtomReference? OverAtom { get; } = over; + public ClickParams Params { get; } = clickParams; } [Serializable, NetSerializable] - public sealed class StatClickedEvent : EntityEventArgs, IAtomClickedEvent { - public string AtomRef; - public bool Middle { get; } - public bool Shift { get; } - public bool Ctrl { get; } - public bool Alt { get; } + public sealed class StatClickedEvent(string atomRef, bool middle, bool shift, bool ctrl, bool alt) + : EntityEventArgs, IAtomMouseEvent { + public string AtomRef = atomRef; // TODO: icon-x and icon-y - public int IconX => 0; - public int IconY => 0; - - // This doesn't seem to appear at all in the click params - public ScreenLocation ScreenLoc => new(0, 0, 32); - - public StatClickedEvent(string atomRef, bool middle, bool shift, bool ctrl, bool alt) { - AtomRef = atomRef; - Middle = middle; - Shift = shift; - Ctrl = ctrl; - Alt = alt; - } + // TODO: ScreenLoc doesn't appear at all in the click params + public ClickParams Params { get; } = new(new(0, 0, 32), middle, shift, ctrl, alt, 0, 0); } } From 3846bfa09a4e50e1f85fac8619f87d845655510d Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:17:50 +0000 Subject: [PATCH 40/64] Makes `client.images` respect changes (#1565) * kinda working I guess * oh wait I can just update the appearance * call parents * client.images test * boom * fix * remove viscontents test from testgame * move client image to session only, and remove properly * Remove unneeded dict assignment * same shitcode in rendertarget rental * Actually, vis_contents += image is a bug! --- .../Rendering/ClientImagesSystem.cs | 53 +++++++------------ OpenDreamClient/Rendering/DreamViewOverlay.cs | 18 +++---- .../Objects/Types/DreamObjectImage.cs | 31 +++++++++++ .../Rendering/ServerClientImagesSystem.cs | 21 ++++---- .../Rendering/SharedClientImagesSystem.cs | 12 ++--- TestGame/code.dm | 4 +- 6 files changed, 80 insertions(+), 59 deletions(-) diff --git a/OpenDreamClient/Rendering/ClientImagesSystem.cs b/OpenDreamClient/Rendering/ClientImagesSystem.cs index a7be99237e..81d702ff9c 100644 --- a/OpenDreamClient/Rendering/ClientImagesSystem.cs +++ b/OpenDreamClient/Rendering/ClientImagesSystem.cs @@ -4,15 +4,13 @@ using Vector3 = Robust.Shared.Maths.Vector3; namespace OpenDreamClient.Rendering; - internal sealed class ClientImagesSystem : SharedClientImagesSystem { [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!; - private readonly Dictionary> _turfClientImages = new(); - private readonly Dictionary> _amClientImages = new(); - private readonly Dictionary _idToIcon = new(); + private readonly Dictionary> _turfClientImages = new(); + private readonly Dictionary> _amClientImages = new(); public override void Initialize() { SubscribeNetworkEvent(OnAddClientImage); @@ -22,23 +20,16 @@ public override void Initialize() { public override void Shutdown() { _turfClientImages.Clear(); _amClientImages.Clear(); - _idToIcon.Clear(); } - public bool TryGetClientImages(EntityUid entity, Vector3? tileCoords, [NotNullWhen(true)] out List? result){ - result = null; - List? resultIDs; + public bool TryGetClientImages(EntityUid entity, Vector3? tileCoords, [NotNullWhen(true)] out List? result){ if(entity == EntityUid.Invalid && tileCoords is not null) { - if(!_turfClientImages.TryGetValue(tileCoords.Value, out resultIDs)) + if(!_turfClientImages.TryGetValue(tileCoords.Value, out result)) return false; } else { - if(!_amClientImages.TryGetValue(entity, out resultIDs)) + if(!_amClientImages.TryGetValue(entity, out result)) return false; } - result = new List(); - foreach(int distinctID in resultIDs) - if(_idToIcon.TryGetValue(distinctID, out DreamIcon? icon)) - result.Add(icon); return result.Count > 0; } @@ -46,21 +37,13 @@ private void OnAddClientImage(AddClientImageEvent e) { EntityUid ent = _entityManager.GetEntity(e.AttachedEntity); if(ent == EntityUid.Invalid) { if(!_turfClientImages.TryGetValue(e.TurfCoords, out var iconList)) - iconList = new List(); - if(!_idToIcon.ContainsKey(e.ImageAppearance)){ - DreamIcon icon = new DreamIcon(_gameTiming, _appearanceSystem, e.ImageAppearance); - _idToIcon[e.ImageAppearance] = icon; - } - iconList.Add(e.ImageAppearance); + iconList = new List(); + iconList.Add(e.ImageEntity); _turfClientImages[e.TurfCoords] = iconList; } else { if(!_amClientImages.TryGetValue(ent, out var iconList)) - iconList = new List(); - if(!_idToIcon.ContainsKey(e.ImageAppearance)){ - DreamIcon icon = new DreamIcon(_gameTiming, _appearanceSystem, e.ImageAppearance); - _idToIcon[e.ImageAppearance] = icon; - } - iconList.Add(e.ImageAppearance); + iconList = new List(); + iconList.Add(e.ImageEntity); _amClientImages[ent] = iconList; } @@ -69,17 +52,19 @@ private void OnAddClientImage(AddClientImageEvent e) { private void OnRemoveClientImage(RemoveClientImageEvent e) { EntityUid ent = _entityManager.GetEntity(e.AttachedEntity); if(ent == EntityUid.Invalid) { - if(!_turfClientImages.TryGetValue(e.TurfCoords, out var iconList)) - return; - iconList.Remove(e.ImageAppearance); - _turfClientImages[e.TurfCoords] = iconList; - _idToIcon.Remove(e.ImageAppearance); + if(!_turfClientImages.TryGetValue(e.TurfCoords, out var iconList)) + return; + iconList.Remove(e.ImageEntity); + if(iconList.Count == 0) + _turfClientImages.Remove(e.TurfCoords); + } else { if(!_amClientImages.TryGetValue(ent, out var iconList)) return; - iconList.Remove(e.ImageAppearance); - _amClientImages[ent] = iconList; - _idToIcon.Remove(e.ImageAppearance); + iconList.Remove(e.ImageEntity); + if(iconList.Count == 0) + _amClientImages.Remove(ent); + } } } diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 89ba23b6b6..b607d763f5 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -304,14 +304,17 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u //client images act as either an overlay or replace the main icon //notably they cannot be applied to overlays, so don't check for them if this is an under/overlay //note also that we use turfCoords and not current.Position because we want world-coordinates, not screen coordinates. This is only used for turfs. - if(parentIcon == null && _clientImagesSystem.TryGetClientImages(current.Uid, turfCoords, out List? attachedClientImages)){ - foreach(DreamIcon CI in attachedClientImages){ - if(CI.Appearance == null) + if(parentIcon == null && _clientImagesSystem.TryGetClientImages(current.Uid, turfCoords, out List? attachedClientImages)){ + foreach(NetEntity CINetEntity in attachedClientImages){ + EntityUid imageEntity = _entityManager.GetEntity(CINetEntity); + if (!_spriteQuery.TryGetComponent(imageEntity, out var sprite)) continue; - if(CI.Appearance.Override) - current.MainIcon = CI; + if(sprite.Icon.Appearance == null) + continue; + if(sprite.Icon.Appearance.Override) + current.MainIcon = sprite.Icon; else - ProcessIconComponents(CI, current.Position, uid, isScreen, ref tieBreaker, result, current, false); + ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, current, false); } } @@ -360,8 +363,6 @@ private IRenderTexture RentRenderTarget(Vector2i size) { } else { result = _clyde.CreateRenderTarget(size, new(RenderTargetColorFormat.Rgba8Srgb)); } - - _renderTargetCache[size] = listResult; //put the shorter list back } return result; @@ -372,7 +373,6 @@ private void ReturnRenderTarget(IRenderTexture rental) { storeList = new List(4); storeList.Add(rental); - _renderTargetCache[rental.Size] = storeList; } private void ClearRenderTarget(IRenderTexture target, DrawingHandleWorld handle, Color clearColor) { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs index 1c554b254a..9847f87534 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs @@ -1,5 +1,7 @@ using OpenDreamRuntime.Procs; +using OpenDreamRuntime.Rendering; using OpenDreamShared.Dream; +using Robust.Shared.Map; namespace OpenDreamRuntime.Objects.Types; @@ -9,6 +11,7 @@ public sealed class DreamObjectImage : DreamObject { private DreamObject? _loc; private DreamList _overlays; private DreamList _underlays; + private EntityUid _entity = EntityUid.Invalid; /// /// All the args in /image/New() after "icon" and "loc", in their correct order @@ -100,6 +103,10 @@ protected override void SetVar(string varName, DreamValue value) { newAppearance.Direction = Appearance!.Direction; Appearance = newAppearance; + if(_entity != EntityUid.Invalid) { + DMISpriteComponent sprite = EntityManager.GetComponent(_entity); + sprite.SetAppearance(Appearance!); + } break; case "loc": value.TryGetValueAsDreamObject(out _loc); @@ -178,6 +185,10 @@ protected override void SetVar(string varName, DreamValue value) { default: if (AtomManager.IsValidAppearanceVar(varName)) { AtomManager.SetAppearanceVar(Appearance!, varName, value); + if(_entity != EntityUid.Invalid) { + DMISpriteComponent sprite = EntityManager.GetComponent(_entity); + sprite.SetAppearance(Appearance!); + } break; } @@ -189,4 +200,24 @@ protected override void SetVar(string varName, DreamValue value) { public DreamObject? GetAttachedLoc(){ return this._loc; } + + /// + /// Get or create the entity associated with this image. Used for putting this image in the world ie, with vis_contents + /// The associated entity is deleted when the image is. + /// + public EntityUid GetEntity() { + if(_entity == EntityUid.Invalid) { + _entity = EntityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); + DMISpriteComponent sprite = EntityManager.AddComponent(_entity); + sprite.SetAppearance(Appearance!); + } + return _entity; + } + + protected override void HandleDeletion() { + if(_entity != EntityUid.Invalid) { + EntityManager.DeleteEntity(_entity); + } + base.HandleDeletion(); + } } diff --git a/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs b/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs index 5da3babaf0..5faef9180d 100644 --- a/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs @@ -2,12 +2,12 @@ using OpenDreamShared.Rendering; using OpenDreamRuntime.Objects; using Vector3 = Robust.Shared.Maths.Vector3; +using Robust.Server.GameStates; namespace OpenDreamRuntime.Rendering; public sealed class ServerClientImagesSystem : SharedClientImagesSystem { - [Dependency] private readonly ServerAppearanceSystem _serverAppearanceSystem = default!; - [Dependency] private readonly AtomManager _atomManager = default!; + [Dependency] private readonly PvsOverrideSystem _pvsOverrideSystem = default!; public void AddImageObject(DreamConnection connection, DreamObjectImage imageObject) { DreamObject? loc = imageObject.GetAttachedLoc(); if(loc == null) @@ -15,9 +15,6 @@ public void AddImageObject(DreamConnection connection, DreamObjectImage imageObj EntityUid locEntity = EntityUid.Invalid; Vector3 turfCoords = Vector3.Zero; - int locAppearanceID = 0; - - int imageAppearanceID = _serverAppearanceSystem.AddAppearance(imageObject.Appearance!); if(loc is DreamObjectMovable movable) locEntity = movable.Entity; @@ -25,7 +22,11 @@ public void AddImageObject(DreamConnection connection, DreamObjectImage imageObj turfCoords = new Vector3(turf.X, turf.Y, turf.Z); NetEntity ent = GetNetEntity(locEntity); - RaiseNetworkEvent(new AddClientImageEvent(ent, turfCoords, imageAppearanceID), connection.Session.ConnectedClient); + EntityUid imageObjectEntity = imageObject.GetEntity(); + NetEntity imageObjectNetEntity = GetNetEntity(imageObjectEntity); + if (imageObjectEntity != EntityUid.Invalid) + _pvsOverrideSystem.AddSessionOverride(imageObjectEntity, connection.Session!); + RaiseNetworkEvent(new AddClientImageEvent(ent, turfCoords, imageObjectNetEntity), connection.Session!.Channel); } public void RemoveImageObject(DreamConnection connection, DreamObjectImage imageObject) { @@ -36,8 +37,6 @@ public void RemoveImageObject(DreamConnection connection, DreamObjectImage image EntityUid locEntity = EntityUid.Invalid; Vector3 turfCoords = Vector3.Zero; - int imageAppearanceID = _serverAppearanceSystem.AddAppearance(imageObject.Appearance!); - if(loc is DreamObjectMovable) locEntity = ((DreamObjectMovable)loc).Entity; else if(loc is DreamObjectTurf turf) @@ -45,6 +44,10 @@ public void RemoveImageObject(DreamConnection connection, DreamObjectImage image NetEntity ent = GetNetEntity(locEntity); - RaiseNetworkEvent(new RemoveClientImageEvent(ent, turfCoords, imageAppearanceID), connection.Session.ConnectedClient); + EntityUid imageObjectEntity = imageObject.GetEntity(); + if (imageObjectEntity != EntityUid.Invalid) + _pvsOverrideSystem.RemoveSessionOverride(imageObjectEntity, connection.Session!); + NetEntity imageObjectNetEntity = GetNetEntity(imageObject.GetEntity()); + RaiseNetworkEvent(new RemoveClientImageEvent(ent, turfCoords, imageObjectNetEntity), connection.Session!.Channel); } } diff --git a/OpenDreamShared/Rendering/SharedClientImagesSystem.cs b/OpenDreamShared/Rendering/SharedClientImagesSystem.cs index c1505ad822..2957f1184c 100644 --- a/OpenDreamShared/Rendering/SharedClientImagesSystem.cs +++ b/OpenDreamShared/Rendering/SharedClientImagesSystem.cs @@ -12,11 +12,11 @@ public class SharedClientImagesSystem : EntitySystem { public sealed class AddClientImageEvent : EntityEventArgs { public Vector3 TurfCoords; public NetEntity AttachedEntity; //if this is NetEntity.Invalid (ie, a turf) use the TurfCoords instead - public int ImageAppearance; + public NetEntity ImageEntity; - public AddClientImageEvent(NetEntity attachedEntity, Vector3 turfCoords, int imageAppearance) { + public AddClientImageEvent(NetEntity attachedEntity, Vector3 turfCoords, NetEntity imageEntity) { TurfCoords = turfCoords; - ImageAppearance = imageAppearance; + ImageEntity = imageEntity; AttachedEntity = attachedEntity; } } @@ -25,11 +25,11 @@ public AddClientImageEvent(NetEntity attachedEntity, Vector3 turfCoords, int ima public sealed class RemoveClientImageEvent : EntityEventArgs { public Vector3 TurfCoords; public NetEntity AttachedEntity; //if this is NetEntity.Invalid (ie, a turf) use the TurfCoords instead - public int ImageAppearance; + public NetEntity ImageEntity; - public RemoveClientImageEvent(NetEntity attachedEntity, Vector3 turfCoords, int imageAppearance) { + public RemoveClientImageEvent(NetEntity attachedEntity, Vector3 turfCoords, NetEntity imageEntity) { TurfCoords = turfCoords; - ImageAppearance = imageAppearance; + ImageEntity = imageEntity; AttachedEntity = attachedEntity; } } diff --git a/TestGame/code.dm b/TestGame/code.dm index 78bff3a7b0..2feb582aef 100644 --- a/TestGame/code.dm +++ b/TestGame/code.dm @@ -37,7 +37,6 @@ New() ..() loc = locate(5, 5, 1) - //color = rgb(rand(0,255), rand(0,255), rand(0,255)) Login() world.log << "login ran" @@ -181,6 +180,9 @@ for(var/turf/T in range(src, 2)) var/image/turf_image = image(icon = 'icons/hanoi.dmi', loc=T, icon_state="1") src.client.images += turf_image + spawn(25) + src << "changing image" + i.icon_state = "5" spawn(50) src.client.images.Cut() From 066e3050fd150519aaaf493954d2bb003e9a52f3 Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 4 Jan 2024 19:09:30 -0800 Subject: [PATCH 41/64] Replace `BitConverter.GetBytes()` call with bit operations (#1609) --- OpenDreamClient/Rendering/DreamViewOverlay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index b607d763f5..4f3cd00db2 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -476,8 +476,11 @@ private void ClearRenderTarget(IRenderTexture target, DrawingHandleWorld handle, Action? mouseMapDrawAction; //setup the MouseMapLookup shader for use in DrawIcon() - byte[] rgba = BitConverter.GetBytes(iconMetaData.GetHashCode()); - Color targetColor = new Color(rgba[0], rgba[1], rgba[2]); //TODO - this could result in mis-clicks due to hash-collision since we ditch a whole byte. + int hash = iconMetaData.GetHashCode(); + var colorR = (byte)(hash & 0xFF); + var colorG = (byte)((hash >> 8) & 0xFF); + var colorB = (byte)((hash >> 16) & 0xFF); + Color targetColor = new Color(colorR, colorG, colorB); //TODO - this could result in mis-clicks due to hash-collision since we ditch a whole byte. MouseMapLookup[targetColor] = iconMetaData; //go fast when the only filter is color, and we don't have more color things to consider From d83ae6d0d2bc5f6a7f5c8f3c5b34291e1890d122 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Sun, 7 Jan 2024 19:16:36 +0000 Subject: [PATCH 42/64] Fully implement splittext and add test (#1560) * fully implement splittext and add test * test list comparison * fix include_delimiters * array instead of linq * Update OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs Co-authored-by: wixoa * duh * I am dum --------- Co-authored-by: wixoa --- .../DMProject/Tests/Text/Splittext.dm | 34 +++++++++++ .../Procs/Native/DreamProcNativeRoot.cs | 61 ++++++++++++++++--- 2 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Text/Splittext.dm diff --git a/Content.Tests/DMProject/Tests/Text/Splittext.dm b/Content.Tests/DMProject/Tests/Text/Splittext.dm new file mode 100644 index 0000000000..da2d898418 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Text/Splittext.dm @@ -0,0 +1,34 @@ +/proc/RunTest() + var/test_text = "The average of 1, 2, 3, 4, 5 is: 3" + var/list/test1 = splittext(test_text, " ") + var/list/test1_expected = list("The","average","of","1,","2,","3,","4,","5","is:","3") + ASSERT(test1 ~= test1_expected) + + var/list/test2 = splittext(test_text, " ", 5) + var/test2_expected = list("average","of","1,","2,","3,","4,","5","is:","3") + ASSERT(test2 ~= test2_expected) + + var/list/test3 = splittext(test_text, " ", 5, 10) + var/test3_expected = list("avera") + ASSERT(test3 ~= test3_expected) + + var/list/test4 = splittext(test_text, " ", 10, 20) + var/test4_expected = list("ge","of","1,","2") + ASSERT(test4 ~= test4_expected) + + var/list/test5 = splittext(test_text, " ", 10, 20, 1) + var/test5_expected = list("ge"," ","of"," ","1,"," ","2") + ASSERT(test5 ~= test5_expected) + + //it's regex time + var/test6 = splittext(test_text, regex(@"\d")) + var/test6_expected = list("The average of ",", ",", ",", ",", "," is: ","") + ASSERT(test6 ~= test6_expected) + + var/test7 = splittext(test_text, regex(@"\d"), 5, 30) + var/test7_expected = list("average of ",", ",", ",", ",", "," ") + ASSERT(test7 ~= test7_expected) + + var/test8 = splittext(test_text, regex(@"\d"), 5, 30, 1) + var/test8_expected = list("average of ","1",", ","2",", ","3",", ","4",", ","5"," ") + ASSERT(test8 ~= test8_expected) \ No newline at end of file diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 224e5b5ea2..b111ac6325 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -2361,22 +2361,65 @@ public static DreamValue NativeProc_splicetext_char(NativeProc.Bundle bundle, Dr [DreamProc("splittext")] [DreamProcParameter("Text", Type = DreamValueTypeFlag.String)] [DreamProcParameter("Delimiter", Type = DreamValueTypeFlag.String)] + [DreamProcParameter("Start", Type = DreamValueTypeFlag.Float, DefaultValue = 1)] + [DreamProcParameter("End", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + [DreamProcParameter("include_delimiters", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] public static DreamValue NativeProc_splittext(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { if (!bundle.GetArgument(0, "Text").TryGetValueAsString(out var text)) { return new DreamValue(bundle.ObjectTree.CreateList()); } - var arg2 = bundle.GetArgument(1, "Delimiter"); - if (!arg2.TryGetValueAsString(out var delimiter)) { - if (!arg2.Equals(DreamValue.Null)) { - return new DreamValue(bundle.ObjectTree.CreateList()); + int start = 0; + int end = 0; + if(bundle.GetArgument(2, "Start").TryGetValueAsInteger(out start)) + start -= 1; //1-indexed + if(bundle.GetArgument(3, "End").TryGetValueAsInteger(out end)) + if(end == 0) + end = text.Length; + else + end -= 1; //1-indexed + bool includeDelimiters = false; + if(bundle.GetArgument(4, "include_delimiters").TryGetValueAsInteger(out var includeDelimitersInt)) + includeDelimiters = includeDelimitersInt != 0; //idk why BYOND doesn't just use truthiness, but it doesn't, so... + + if(start > 0 || end < text.Length) + text = text[Math.Max(start,0)..Math.Min(end, text.Length)]; + + var delim = bundle.GetArgument(1, "Delimiter"); //can either be a regex or string + + if (delim.TryGetValueAsDreamObject(out var regexObject)) { + if(includeDelimiters) { + var values = new List(); + int pos = 0; + foreach (Match m in regexObject.Regex.Matches(text)) { + values.Add(text.Substring(pos, m.Index - pos)); + values.Add(m.Value); + pos = m.Index + m.Length; + } + values.Add(text.Substring(pos)); + return new DreamValue(bundle.ObjectTree.CreateList(values.ToArray())); + } else { + return new DreamValue(bundle.ObjectTree.CreateList(regexObject.Regex.Split(text))); + } + } else if (delim.TryGetValueAsString(out var delimiter)) { + string[] splitText; + if(includeDelimiters) { + //basically split on delimeter, and then add the delimiter back in after each split (except the last one) + splitText= text.Split(delimiter); + string[] longerSplitText = new string[splitText.Length * 2 - 1]; + for(int i = 0; i < splitText.Length; i++) { + longerSplitText[i * 2] = splitText[i]; + if(i < splitText.Length - 1) + longerSplitText[i * 2 + 1] = delimiter; + } + splitText = longerSplitText; + } else { + splitText = text.Split(delimiter); } + return new DreamValue(bundle.ObjectTree.CreateList(splitText)); + } else { + return new DreamValue(bundle.ObjectTree.CreateList()); } - - string[] splitText = text.Split(delimiter); - DreamList list = bundle.ObjectTree.CreateList(splitText); - - return new DreamValue(list); } private static void OutputToStatPanel(DreamManager dreamManager, DreamConnection connection, DreamValue name, DreamValue value) { From cf334d606eb1e029dc76920441b1b17543ccae30 Mon Sep 17 00:00:00 2001 From: distributivgesetz Date: Tue, 9 Jan 2024 21:39:06 +0100 Subject: [PATCH 43/64] Refactor dereference operations into tagged unions (#1603) * refactor deref * add IsFuzzy prop and eliminate CompileErrorExceptions * s * stop complaining * forgot to throw here * more code improvements in ParseDereference * remove notimplementedexceptions here * review comments + added some docs --- DMCompiler/Compiler/DM/DMAST.cs | 64 ++-- DMCompiler/Compiler/DM/DMParser.cs | 112 +++---- DMCompiler/DM/DMExpression.cs | 6 + DMCompiler/DM/Expressions/Binary.cs | 2 + DMCompiler/DM/Expressions/Builtins.cs | 12 + DMCompiler/DM/Expressions/Dereference.cs | 310 +++++++----------- DMCompiler/DM/Expressions/Procs.cs | 2 + DMCompiler/DM/Expressions/Ternary.cs | 2 + DMCompiler/DM/Visitors/DMASTSimplifier.cs | 23 +- DMCompiler/DM/Visitors/DMExpressionBuilder.cs | 280 +++++++--------- 10 files changed, 368 insertions(+), 445 deletions(-) diff --git a/DMCompiler/Compiler/DM/DMAST.cs b/DMCompiler/Compiler/DM/DMAST.cs index 9ddaadd626..59f0f5c246 100644 --- a/DMCompiler/Compiler/DM/DMAST.cs +++ b/DMCompiler/Compiler/DM/DMAST.cs @@ -2693,34 +2693,42 @@ public override void Visit(DMASTVisitor visitor) { public sealed class DMASTDereference : DMASTExpression { - public enum OperationKind { - Invalid, - - Field, // x.y - FieldSafe, // x?.y - FieldSearch, // x:y - FieldSafeSearch, // x?:y - - Index, // x[y] - IndexSafe, // x?[y] - - Call, // x.y() - CallSafe, // x?.y() - CallSearch, // x:y() - CallSafeSearch, // x?:y() - } - - public struct Operation { - public OperationKind Kind; - - // Field*, Call* - public DMASTIdentifier Identifier; - - // Index* - public DMASTExpression Index; - - // Call* - public DMASTCallParameter[] Parameters; + public abstract class Operation { + /// + /// The location of the operation. + /// + public required Location Location; + /// + /// Whether we should short circuit if the expression we are accessing is null. + /// + public required bool Safe; // x?.y, x?.y() etc + } + + public abstract class NamedOperation : Operation { + /// + /// Name of the identifier. + /// + public required string Identifier; + /// + /// Whether we should check if the variable exists or not. + /// + public required bool NoSearch; // x:y, x:y() + } + + public sealed class FieldOperation : NamedOperation; + + public sealed class IndexOperation : Operation { + /// + /// The index expression that we use to index this expression (constant or otherwise). + /// + public required DMASTExpression Index; // x[y], x?[y] + } + + public sealed class CallOperation : NamedOperation { + /// + /// The parameters that we call this proc with. + /// + public required DMASTCallParameter[] Parameters; // x.y(), } public DMASTExpression Expression; diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index a163037474..a4bc611c20 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using DMCompiler.Compiler.DMPreprocessor; using OpenDreamShared.Compiler; using OpenDreamShared.Dream; +using System; +using System.Collections.Generic; +using System.Linq; using String = System.String; namespace DMCompiler.Compiler.DM { @@ -2220,15 +2220,13 @@ private void BracketWhitespace() { Token token = Current(); // Check for a valid deref operation token - { - if (!Check(DereferenceTypes)) { - Whitespace(); + if (!Check(DereferenceTypes)) { + Whitespace(); - token = Current(); + token = Current(); - if (!Check(WhitespacedDereferenceTypes)) { - break; - } + if (!Check(WhitespacedDereferenceTypes)) { + break; } } @@ -2259,46 +2257,49 @@ private void BracketWhitespace() { break; } - DMASTDereference.Operation operation = new() { - Kind = DMASTDereference.OperationKind.Invalid, - }; + DMASTDereference.Operation operation; switch (token.Type) { case TokenType.DM_Period: case TokenType.DM_QuestionPeriod: case TokenType.DM_Colon: case TokenType.DM_QuestionColon: { - DMASTIdentifier identifier = Identifier(); + var identifier = Identifier(); - operation.Kind = token.Type switch { - TokenType.DM_Period => DMASTDereference.OperationKind.Field, - TokenType.DM_QuestionPeriod => DMASTDereference.OperationKind.FieldSafe, - TokenType.DM_Colon => DMASTDereference.OperationKind.FieldSearch, - TokenType.DM_QuestionColon => DMASTDereference.OperationKind.FieldSafeSearch, - _ => throw new InvalidOperationException(), - }; - - operation.Identifier = identifier; + if (identifier == null) { + DMCompiler.Emit(WarningCode.BadToken, token.Location, "Identifier expected"); + return new DMASTConstantNull(token.Location); } + + operation = new DMASTDereference.FieldOperation { + Location = identifier.Location, + Safe = token.Type is TokenType.DM_QuestionPeriod or TokenType.DM_QuestionColon, + Identifier = identifier.Identifier, + NoSearch = token.Type is TokenType.DM_Colon or TokenType.DM_QuestionColon + }; break; + } case TokenType.DM_LeftBracket: case TokenType.DM_QuestionLeftBracket: { - ternaryBHasPriority = true; - - Whitespace(); - DMASTExpression index = Expression(); - ConsumeRightBracket(); + ternaryBHasPriority = true; - operation.Kind = token.Type switch { - TokenType.DM_LeftBracket => DMASTDereference.OperationKind.Index, - TokenType.DM_QuestionLeftBracket => DMASTDereference.OperationKind.IndexSafe, - _ => throw new InvalidOperationException(), - }; + Whitespace(); + var index = Expression(); + ConsumeRightBracket(); - operation.Index = index; + if (index == null) { + DMCompiler.Emit(WarningCode.BadToken, token.Location, "Expression expected"); + return new DMASTConstantNull(token.Location); } + + operation = new DMASTDereference.IndexOperation { + Index = index, + Location = index.Location, + Safe = token.Type is TokenType.DM_QuestionLeftBracket + }; break; + } default: throw new InvalidOperationException("unhandled dereference token"); @@ -2308,39 +2309,26 @@ private void BracketWhitespace() { if (allowCalls) { Whitespace(); - DMASTCallParameter[] parameters = ProcCall(); + var parameters = ProcCall(); if (parameters != null) { ternaryBHasPriority = true; - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - operation.Kind = DMASTDereference.OperationKind.Call; - operation.Parameters = parameters; - break; - - case DMASTDereference.OperationKind.FieldSafe: - operation.Kind = DMASTDereference.OperationKind.CallSafe; - operation.Parameters = parameters; - break; - - case DMASTDereference.OperationKind.FieldSearch: - operation.Kind = DMASTDereference.OperationKind.CallSearch; - operation.Parameters = parameters; - break; - - case DMASTDereference.OperationKind.FieldSafeSearch: - operation.Kind = DMASTDereference.OperationKind.CallSafeSearch; - operation.Parameters = parameters; + switch (operation) { + case DMASTDereference.FieldOperation fieldOperation: + operation = new DMASTDereference.CallOperation { + Parameters = parameters, + Location = fieldOperation.Location, + Safe = fieldOperation.Safe, + Identifier = fieldOperation.Identifier, + NoSearch = fieldOperation.NoSearch + }; break; - case DMASTDereference.OperationKind.Index: - case DMASTDereference.OperationKind.IndexSafe: - Error("attempt to call an invalid l-value"); - return null; + case DMASTDereference.IndexOperation: + DMCompiler.Emit(WarningCode.BadToken, token.Location, "Attempt to call an invalid l-value"); + return new DMASTConstantNull(token.Location); - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSafe: default: throw new InvalidOperationException("unhandled dereference operation kind"); } @@ -2350,7 +2338,7 @@ private void BracketWhitespace() { operations.Add(operation); } - if (operations.Any()) { + if (operations.Count != 0) { Whitespace(); return new DMASTDereference(expression.Location, expression, operations.ToArray()); } @@ -2360,7 +2348,7 @@ private void BracketWhitespace() { return expression; } - private DMASTExpression ParseProcCall(DMASTExpression expression) { + private DMASTExpression? ParseProcCall(DMASTExpression? expression) { if (expression is not (DMASTCallable or DMASTIdentifier or DMASTGlobalIdentifier)) return expression; Whitespace(); diff --git a/DMCompiler/DM/DMExpression.cs b/DMCompiler/DM/DMExpression.cs index 78cffa0001..31055cb394 100644 --- a/DMCompiler/DM/DMExpression.cs +++ b/DMCompiler/DM/DMExpression.cs @@ -64,6 +64,12 @@ public virtual string GetNameof(DMObject dmObject, DMProc proc) { throw new CompileAbortException(Location, "nameof: requires a var, proc reference, or type path"); } + /// + /// Determines whether the expression returns an ambiguous path. + /// + /// Dereferencing these expressions will always skip validation via the "expr:y" operation. + public virtual bool PathIsFuzzy => false; + public virtual DreamPath? Path => null; public virtual DreamPath? NestedPath => Path; diff --git a/DMCompiler/DM/Expressions/Binary.cs b/DMCompiler/DM/Expressions/Binary.cs index 6a29020177..2853445b41 100644 --- a/DMCompiler/DM/Expressions/Binary.cs +++ b/DMCompiler/DM/Expressions/Binary.cs @@ -214,6 +214,8 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { // x & y sealed class BinaryAnd : BinaryOp { + public override bool PathIsFuzzy => true; + public BinaryAnd(Location location, DMExpression lhs, DMExpression rhs) : base(location, lhs, rhs) { } diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index ab46a0fc31..1a6c68108a 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -48,6 +48,8 @@ sealed class New : DMExpression { private readonly DMExpression _expr; private readonly ArgumentList _arguments; + public override bool PathIsFuzzy => Path == null; + public New(Location location, DMExpression expr, ArgumentList arguments) : base(location) { _expr = expr; _arguments = arguments; @@ -345,6 +347,8 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { internal sealed class IsNull : DMExpression { private readonly DMExpression _value; + public override bool PathIsFuzzy => true; + public IsNull(Location location, DMExpression value) : base(location) { _value = value; } @@ -359,6 +363,8 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { internal sealed class Length : DMExpression { private readonly DMExpression _value; + public override bool PathIsFuzzy => true; + public Length(Location location, DMExpression value) : base(location) { _value = value; } @@ -374,6 +380,8 @@ internal sealed class GetStep : DMExpression { private readonly DMExpression _ref; private readonly DMExpression _dir; + public override bool PathIsFuzzy => true; + public GetStep(Location location, DMExpression refValue, DMExpression dir) : base(location) { _ref = refValue; _dir = dir; @@ -391,6 +399,8 @@ internal sealed class GetDir : DMExpression { private readonly DMExpression _loc1; private readonly DMExpression _loc2; + public override bool PathIsFuzzy => true; + public GetDir(Location location, DMExpression loc1, DMExpression loc2) : base(location) { _loc1 = loc1; _loc2 = loc2; @@ -408,6 +418,8 @@ sealed class List : DMExpression { private readonly (DMExpression? Key, DMExpression Value)[] _values; private readonly bool _isAssociative; + public override bool PathIsFuzzy => true; + public List(Location location, (DMExpression? Key, DMExpression Value)[] values) : base(location) { _values = values; diff --git a/DMCompiler/DM/Expressions/Dereference.cs b/DMCompiler/DM/Expressions/Dereference.cs index 1a5c1233e5..f3ccf66788 100644 --- a/DMCompiler/DM/Expressions/Dereference.cs +++ b/DMCompiler/DM/Expressions/Dereference.cs @@ -1,31 +1,49 @@ +using DMCompiler.Bytecode; using OpenDreamShared.Compiler; -using DMCompiler.Compiler.DM; using OpenDreamShared.Dream; using System; -using DMCompiler.Bytecode; +using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace DMCompiler.DM.Expressions { // x.y.z // x[y][z] // x.f().y.g()[2] // etc. - class Dereference : LValue { - public struct Operation { - public DMASTDereference.OperationKind Kind; - - // Field*, Call* - public string Identifier; + internal class Dereference : LValue { + public abstract class Operation { + /// + /// Whether this operation will short circuit if the dereference equals null. (equal to x?.y) + /// + public required bool Safe { get; init; } + + /// + /// The path of the l-value being dereferenced. + /// + public DreamPath? Path { get; init; } + } - // Field* - public int? GlobalId; + public abstract class NamedOperation : Operation { + /// + /// The name of the identifier. + /// + public required string Identifier { get; init; } + } - // Index* - public DMExpression Index; + public sealed class FieldOperation : NamedOperation; - // Call* - public ArgumentList Parameters; + public sealed class IndexOperation : Operation { + /// + /// The index expression. (eg. x[expr]) + /// + public required DMExpression Index { get; init; } + } - public DreamPath? Path; + public sealed class CallOperation : NamedOperation { + /// + /// The argument list inside the call operation's parentheses. (eg. x(args, ...)) + /// + public required ArgumentList Parameters { get; init; } } private readonly DMExpression _expression; @@ -33,6 +51,7 @@ public struct Operation { public override DreamPath? Path { get; } public override DreamPath? NestedPath { get; } + public override bool PathIsFuzzy => Path == null; public Dereference(Location location, DreamPath? path, DMExpression expression, Operation[] operations) : base(location, null) { @@ -60,49 +79,34 @@ private void ShortCircuitHandler(DMProc proc, string endLabel, ShortCircuitMode } } - private void EmitOperation(DMObject dmObject, DMProc proc, ref Operation operation, string endLabel, ShortCircuitMode shortCircuitMode) { - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSearch: - proc.DereferenceField(operation.Identifier); - break; - - case DMASTDereference.OperationKind.FieldSafe: - case DMASTDereference.OperationKind.FieldSafeSearch: - ShortCircuitHandler(proc, endLabel, shortCircuitMode); - proc.DereferenceField(operation.Identifier); - break; - - case DMASTDereference.OperationKind.Index: - operation.Index.EmitPushValue(dmObject, proc); - proc.DereferenceIndex(); + private void EmitOperation(DMObject dmObject, DMProc proc, Operation operation, string endLabel, ShortCircuitMode shortCircuitMode) { + switch (operation) { + case FieldOperation fieldOperation: + if (fieldOperation.Safe) { + ShortCircuitHandler(proc, endLabel, shortCircuitMode); + } + proc.DereferenceField(fieldOperation.Identifier); break; - case DMASTDereference.OperationKind.IndexSafe: - ShortCircuitHandler(proc, endLabel, shortCircuitMode); - operation.Index.EmitPushValue(dmObject, proc); + case IndexOperation indexOperation: + if (indexOperation.Safe) { + ShortCircuitHandler(proc, endLabel, shortCircuitMode); + } + indexOperation.Index.EmitPushValue(dmObject, proc); proc.DereferenceIndex(); break; - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSearch: { - var (argumentsType, argumentStackSize) = operation.Parameters.EmitArguments(dmObject, proc); - proc.DereferenceCall(operation.Identifier, argumentsType, argumentStackSize); - break; - } - - case DMASTDereference.OperationKind.CallSafe: - case DMASTDereference.OperationKind.CallSafeSearch: { - ShortCircuitHandler(proc, endLabel, shortCircuitMode); - var (argumentsType, argumentStackSize) = operation.Parameters.EmitArguments(dmObject, proc); - proc.DereferenceCall(operation.Identifier, argumentsType, argumentStackSize); + case CallOperation callOperation: + if (callOperation.Safe) { + ShortCircuitHandler(proc, endLabel, shortCircuitMode); + } + var (argumentsType, argumentStackSize) = callOperation.Parameters.EmitArguments(dmObject, proc); + proc.DereferenceCall(callOperation.Identifier, argumentsType, argumentStackSize); break; - } - case DMASTDereference.OperationKind.Invalid: default: - throw new NotImplementedException(); - }; + throw new InvalidOperationException("Unimplemented dereference operation"); + } } public override void EmitPushValue(DMObject dmObject, DMProc proc) { @@ -110,78 +114,49 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { _expression.EmitPushValue(dmObject, proc); - foreach (ref var operation in _operations.AsSpan()) { - EmitOperation(dmObject, proc, ref operation, endLabel, ShortCircuitMode.KeepNull); + foreach (var operation in _operations) { + EmitOperation(dmObject, proc, operation, endLabel, ShortCircuitMode.KeepNull); } proc.AddLabel(endLabel); } public override bool CanReferenceShortCircuit() { - foreach (var operation in _operations) { - switch (operation.Kind) { - case DMASTDereference.OperationKind.FieldSafe: - case DMASTDereference.OperationKind.FieldSafeSearch: - case DMASTDereference.OperationKind.IndexSafe: - case DMASTDereference.OperationKind.CallSafe: - case DMASTDereference.OperationKind.CallSafeSearch: - return true; - - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSearch: - case DMASTDereference.OperationKind.Index: - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSearch: - break; - - case DMASTDereference.OperationKind.Invalid: - default: - throw new NotImplementedException(); - } - } - - return base.CanReferenceShortCircuit(); + return _operations.Any(operation => operation.Safe); } - public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode) { + public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode = ShortCircuitMode.KeepNull) { _expression.EmitPushValue(dmObject, proc); // Perform all except for our last operation for (int i = 0; i < _operations.Length - 1; i++) { - EmitOperation(dmObject, proc, ref _operations[i], endLabel, shortCircuitMode); + EmitOperation(dmObject, proc, _operations[i], endLabel, shortCircuitMode); } - ref var operation = ref _operations[^1]; + var operation = _operations[^1]; - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSearch: - return DMReference.CreateField(operation.Identifier); - - case DMASTDereference.OperationKind.FieldSafe: - case DMASTDereference.OperationKind.FieldSafeSearch: - ShortCircuitHandler(proc, endLabel, shortCircuitMode); - return DMReference.CreateField(operation.Identifier); - - case DMASTDereference.OperationKind.Index: - operation.Index.EmitPushValue(dmObject, proc); - return DMReference.ListIndex; + switch (operation) { + case FieldOperation fieldOperation: + if (fieldOperation.Safe) { + ShortCircuitHandler(proc, endLabel, shortCircuitMode); + } + return DMReference.CreateField(fieldOperation.Identifier); - case DMASTDereference.OperationKind.IndexSafe: - ShortCircuitHandler(proc, endLabel, shortCircuitMode); - operation.Index.EmitPushValue(dmObject, proc); + case IndexOperation indexOperation: + if (indexOperation.Safe) { + ShortCircuitHandler(proc, endLabel, shortCircuitMode); + } + indexOperation.Index.EmitPushValue(dmObject, proc); return DMReference.ListIndex; - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSearch: - case DMASTDereference.OperationKind.CallSafe: - case DMASTDereference.OperationKind.CallSafeSearch: - throw new CompileErrorException(Location, $"attempt to reference proc call result"); + case CallOperation: + DMCompiler.Emit(WarningCode.BadExpression, Location, + "Expected field or index as reference, got proc call result"); + return default; - case DMASTDereference.OperationKind.Invalid: default: - throw new NotImplementedException(); - }; + throw new InvalidOperationException("Unimplemented dereference operation"); + } } public override void EmitPushInitial(DMObject dmObject, DMProc proc) { @@ -191,46 +166,36 @@ public override void EmitPushInitial(DMObject dmObject, DMProc proc) { // Perform all except for our last operation for (int i = 0; i < _operations.Length - 1; i++) { - EmitOperation(dmObject, proc, ref _operations[i], endLabel, ShortCircuitMode.KeepNull); + EmitOperation(dmObject, proc, _operations[i], endLabel, ShortCircuitMode.KeepNull); } - ref var operation = ref _operations[^1]; + var operation = _operations[^1]; - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSearch: - proc.PushString(operation.Identifier); - proc.Initial(); - break; - - case DMASTDereference.OperationKind.FieldSafe: - case DMASTDereference.OperationKind.FieldSafeSearch: - proc.JumpIfNullNoPop(endLabel); - proc.PushString(operation.Identifier); + switch (operation) { + case FieldOperation fieldOperation: + if (fieldOperation.Safe) { + proc.JumpIfNullNoPop(endLabel); + } + proc.PushString(fieldOperation.Identifier); proc.Initial(); break; - case DMASTDereference.OperationKind.Index: - operation.Index.EmitPushValue(dmObject, proc); + case IndexOperation indexOperation: + if (indexOperation.Safe) { + proc.JumpIfNullNoPop(endLabel); + } + indexOperation.Index.EmitPushValue(dmObject, proc); proc.Initial(); break; - case DMASTDereference.OperationKind.IndexSafe: - proc.JumpIfNullNoPop(endLabel); - operation.Index.EmitPushValue(dmObject, proc); - proc.Initial(); + case CallOperation: + DMCompiler.Emit(WarningCode.BadExpression, Location, + "Expected field or index for initial(), got proc call result"); break; - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSearch: - case DMASTDereference.OperationKind.CallSafe: - case DMASTDereference.OperationKind.CallSafeSearch: - throw new CompileErrorException(Location, $"attempt to get `initial` of a proc call"); - - case DMASTDereference.OperationKind.Invalid: default: - throw new NotImplementedException(); - }; + throw new InvalidOperationException("Unimplemented dereference operation"); + } proc.AddLabel(endLabel); } @@ -242,79 +207,56 @@ public void EmitPushIsSaved(DMObject dmObject, DMProc proc) { // Perform all except for our last operation for (int i = 0; i < _operations.Length - 1; i++) { - EmitOperation(dmObject, proc, ref _operations[i], endLabel, ShortCircuitMode.KeepNull); + EmitOperation(dmObject, proc, _operations[i], endLabel, ShortCircuitMode.KeepNull); } - ref var operation = ref _operations[^1]; + var operation = _operations[^1]; - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSearch: - proc.PushString(operation.Identifier); + switch (operation) { + case FieldOperation fieldOperation: + if (fieldOperation.Safe) { + proc.JumpIfNullNoPop(endLabel); + } + proc.PushString(fieldOperation.Identifier); proc.IsSaved(); break; - case DMASTDereference.OperationKind.FieldSafe: - case DMASTDereference.OperationKind.FieldSafeSearch: - proc.JumpIfNullNoPop(endLabel); - proc.PushString(operation.Identifier); + case IndexOperation indexOperation: + if (indexOperation.Safe) { + proc.JumpIfNullNoPop(endLabel); + } + indexOperation.Index.EmitPushValue(dmObject, proc); proc.IsSaved(); break; - case DMASTDereference.OperationKind.Index: - operation.Index.EmitPushValue(dmObject, proc); - proc.IsSaved(); + case CallOperation: + DMCompiler.Emit(WarningCode.BadExpression, Location, + "Expected field or index for issaved(), got proc call result"); break; - case DMASTDereference.OperationKind.IndexSafe: - proc.JumpIfNullNoPop(endLabel); - operation.Index.EmitPushValue(dmObject, proc); - proc.IsSaved(); - break; - - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSearch: - case DMASTDereference.OperationKind.CallSafe: - case DMASTDereference.OperationKind.CallSafeSearch: - throw new CompileErrorException(Location, $"attempt to get `issaved` of a proc call"); - - case DMASTDereference.OperationKind.Invalid: default: - throw new NotImplementedException(); - }; + throw new InvalidOperationException("Unimplemented dereference operation"); + } proc.AddLabel(endLabel); } - public override bool TryAsConstant(out Constant constant) { - DreamPath? prevPath = null; + public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) { + var prevPath = _operations.Length == 1 ? _expression.Path : _operations[^2].Path; - if (_operations.Length == 1) { - prevPath = _expression.Path; - } else { - prevPath = _operations[^2].Path; - } + var operation = _operations[^1]; - ref var operation = ref _operations[^1]; - - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSearch: - case DMASTDereference.OperationKind.FieldSafe: - case DMASTDereference.OperationKind.FieldSafeSearch: - if (prevPath is not null) { - var obj = DMObjectTree.GetDMObject(prevPath.GetValueOrDefault()); - var variable = obj.GetVariable(operation.Identifier); - if (variable != null) { - if (variable.IsConst) - return variable.Value.TryAsConstant(out constant); - if ((variable.ValType & DMValueType.CompiletimeReadonly) == DMValueType.CompiletimeReadonly) { - variable.Value.TryAsConstant(out constant); - return true; // MUST be true. - } - } + if (operation is FieldOperation fieldOperation && prevPath is not null) { + var obj = DMObjectTree.GetDMObject(prevPath.Value); + var variable = obj!.GetVariable(fieldOperation.Identifier); + if (variable != null) { + if (variable.IsConst) + return variable.Value.TryAsConstant(out constant); + if (variable.ValType.HasFlag(DMValueType.CompiletimeReadonly)) { + variable.Value.TryAsConstant(out constant!); + return true; // MUST be true. } - break; + } } constant = null; diff --git a/DMCompiler/DM/Expressions/Procs.cs b/DMCompiler/DM/Expressions/Procs.cs index c12827a2fd..ca16cf3798 100644 --- a/DMCompiler/DM/Expressions/Procs.cs +++ b/DMCompiler/DM/Expressions/Procs.cs @@ -102,6 +102,8 @@ sealed class ProcCall : DMExpression { private readonly DMExpression _target; private readonly ArgumentList _arguments; + public override bool PathIsFuzzy => Path == null; + public ProcCall(Location location, DMExpression target, ArgumentList arguments) : base(location) { _target = target; _arguments = arguments; diff --git a/DMCompiler/DM/Expressions/Ternary.cs b/DMCompiler/DM/Expressions/Ternary.cs index 73e1e9fbce..2ec880c57d 100644 --- a/DMCompiler/DM/Expressions/Ternary.cs +++ b/DMCompiler/DM/Expressions/Ternary.cs @@ -6,6 +6,8 @@ namespace DMCompiler.DM.Expressions { sealed class Ternary : DMExpression { private readonly DMExpression _a, _b, _c; + public override bool PathIsFuzzy => true; + public Ternary(Location location, DMExpression a, DMExpression b, DMExpression c) : base(location) { _a = a; _b = b; diff --git a/DMCompiler/DM/Visitors/DMASTSimplifier.cs b/DMCompiler/DM/Visitors/DMASTSimplifier.cs index 77c8b7b371..1956f95884 100644 --- a/DMCompiler/DM/Visitors/DMASTSimplifier.cs +++ b/DMCompiler/DM/Visitors/DMASTSimplifier.cs @@ -1,4 +1,5 @@ using DMCompiler.Compiler.DM; +using DMCompiler.DM.Expressions; using System; namespace DMCompiler.DM.Visitors { @@ -556,19 +557,19 @@ private void SimplifyExpression(ref DMASTExpression expression) { } } - DMASTDereference deref = expression as DMASTDereference; - if (deref != null) { + if (expression is DMASTDereference deref) { SimplifyExpression(ref deref.Expression); - foreach (ref var operation in deref.Operations.AsSpan()) { - if (operation.Index != null) { - SimplifyExpression(ref deref.Expression); - } - - if (operation.Parameters != null) { - foreach (DMASTCallParameter parameter in operation.Parameters) { - SimplifyExpression(ref parameter.Value); - } + foreach (var operation in deref.Operations) { + switch (operation) { + case DMASTDereference.IndexOperation indexOperation: + SimplifyExpression(ref indexOperation.Index); + break; + case DMASTDereference.CallOperation callOperation: + foreach (var param in callOperation.Parameters) { + SimplifyExpression(ref param.Value); + } + break; } } } diff --git a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs index 2f21a22532..1e0e9e7da7 100644 --- a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs @@ -1,9 +1,9 @@ -using System; +using DMCompiler.Compiler.DM; using DMCompiler.DM.Expressions; using OpenDreamShared.Compiler; -using DMCompiler.Compiler.DM; using OpenDreamShared.Dream; using Robust.Shared.Utility; +using System; namespace DMCompiler.DM.Visitors; @@ -480,72 +480,50 @@ private static DMExpression BuildDereference(DMASTDereference deref, DMObject dm var operations = new Dereference.Operation[deref.Operations.Length]; int astOperationOffset = 0; - static bool IsFuzzy(DMExpression expr) { - switch (expr) { - case Dereference when expr.Path == null: - case ProcCall when expr.Path == null: - case New when expr.Path == null: - case List: - case Ternary: - case BinaryAnd: - case IsNull: - case Length: - case GetStep: - case GetDir: - return true; - default: return false; - } - } - // Path of the previous operation that was iterated over (starting as the base expression) DreamPath? prevPath = expr.Path; - bool pathIsFuzzy = IsFuzzy(expr); + var pathIsFuzzy = expr.PathIsFuzzy; // Special behaviour for `global.x`, `global.vars`, and `global.f()` if (expr is Global) { - ref DMASTDereference.Operation firstOperation = ref astOperations[0]; - - if (firstOperation is { Kind: DMASTDereference.OperationKind.Field, Identifier.Identifier: "vars" }) { - // `global.vars` - expr = new GlobalVars(expr.Location); - - var newOperationCount = operations.Length - 1; - if (newOperationCount == 0) { - return expr; - } - - operations = new Dereference.Operation[newOperationCount]; - astOperationOffset = 1; + DMASTDereference.Operation firstOperation = astOperations[0]; + if (firstOperation is DMASTDereference.NamedOperation namedOperation) { prevPath = null; pathIsFuzzy = true; - } else if (firstOperation.Kind == DMASTDereference.OperationKind.Field) { - // `global.x` - var globalId = dmObject.GetGlobalVariableId(firstOperation.Identifier.Identifier); - if (globalId == null) { - throw new UnknownIdentifierException(deref.Location, $"global.{firstOperation.Identifier.Identifier}"); - } - - var property = DMObjectTree.Globals[globalId.Value]; - expr = new GlobalField(expr.Location, property.Type, globalId.Value); - - var newOperationCount = operations.Length - 1; - if (newOperationCount == 0) { - return expr; - } + switch (namedOperation) { + // global.f() + case DMASTDereference.CallOperation callOperation: + ArgumentList argumentList = new(deref.Expression.Location, dmObject, proc, + callOperation.Parameters); + + var globalProc = new GlobalProc(expr.Location, callOperation.Identifier); + expr = new ProcCall(expr.Location, globalProc, argumentList); + break; + + case DMASTDereference.FieldOperation: + // global.vars + if (namedOperation is { Identifier: "vars" }) { + expr = new GlobalVars(expr.Location); + break; + } - operations = new Dereference.Operation[newOperationCount]; - astOperationOffset = 1; + // global.variable + var globalId = dmObject.GetGlobalVariableId(namedOperation.Identifier); + if (globalId == null) { + throw new UnknownIdentifierException(deref.Location, $"global.{namedOperation.Identifier}"); + } - prevPath = property.Type; - pathIsFuzzy = false; - } else if (firstOperation.Kind == DMASTDereference.OperationKind.Call) { - // `global.f()` - ArgumentList argumentList = new(deref.Expression.Location, dmObject, proc, firstOperation.Parameters); + var property = DMObjectTree.Globals[globalId.Value]; + expr = new GlobalField(expr.Location, property.Type, globalId.Value); - var globalProc = new GlobalProc(expr.Location, firstOperation.Identifier.Identifier); - expr = new ProcCall(expr.Location, globalProc, argumentList); + prevPath = property.Type; + pathIsFuzzy = false; + break; + default: + throw new ArgumentOutOfRangeException($"Missing implementation for {namedOperation}"); + } var newOperationCount = operations.Length - 1; if (newOperationCount == 0) { @@ -554,156 +532,138 @@ static bool IsFuzzy(DMExpression expr) { operations = new Dereference.Operation[newOperationCount]; astOperationOffset = 1; - - prevPath = null; - pathIsFuzzy = true; } else { - throw new CompileErrorException(deref.Location, $"Invalid dereference operation performed on `global`"); + DMCompiler.Emit(WarningCode.BadExpression, firstOperation.Location, + "Invalid dereference operation performed on global"); + expr = new Null(firstOperation.Location); } } for (int i = 0; i < operations.Length; i++) { - ref DMASTDereference.Operation astOperation = ref astOperations[i + astOperationOffset]; - ref Dereference.Operation operation = ref operations[i]; - - operation.Kind = astOperation.Kind; - - // If the last operation evaluated as an ambiguous type, we force the next operation to be a search - if (pathIsFuzzy) { - operation.Kind = operation.Kind switch { - DMASTDereference.OperationKind.Invalid => throw new InvalidOperationException(), - - DMASTDereference.OperationKind.Field => DMASTDereference.OperationKind.FieldSearch, - DMASTDereference.OperationKind.FieldSafe => DMASTDereference.OperationKind.FieldSafeSearch, - DMASTDereference.OperationKind.FieldSearch => DMASTDereference.OperationKind.FieldSearch, - DMASTDereference.OperationKind.FieldSafeSearch => DMASTDereference.OperationKind.FieldSafeSearch, - DMASTDereference.OperationKind.Call => DMASTDereference.OperationKind.CallSearch, - DMASTDereference.OperationKind.CallSafe => DMASTDereference.OperationKind.CallSafeSearch, - DMASTDereference.OperationKind.CallSearch => DMASTDereference.OperationKind.CallSearch, - DMASTDereference.OperationKind.CallSafeSearch => DMASTDereference.OperationKind.CallSafeSearch, - - // Indexes are always fuzzy anyway! - DMASTDereference.OperationKind.Index => DMASTDereference.OperationKind.Index, - DMASTDereference.OperationKind.IndexSafe => DMASTDereference.OperationKind.IndexSafe, - - _ => throw new InvalidOperationException(), - }; - } - switch (operation.Kind) { - case DMASTDereference.OperationKind.Field: - case DMASTDereference.OperationKind.FieldSafe: { - string field = astOperation.Identifier.Identifier; + DMASTDereference.Operation astOperation = astOperations[i + astOperationOffset]; + Dereference.Operation operation; - if (prevPath == null) { - throw new UnknownIdentifierException(deref.Location, field); - } + switch (astOperation) { + case DMASTDereference.FieldOperation fieldOperation: { + var field = fieldOperation.Identifier; - DMObject? fromObject = DMObjectTree.GetDMObject(prevPath.Value, false); - if (fromObject == null) { - throw new CompileErrorException(deref.Location, $"Type {prevPath.Value} does not exist"); - } + DMVariable? property = null; + + // If the last operation evaluated as an ambiguous type, we force the next operation to be a search + if (!fieldOperation.NoSearch && !pathIsFuzzy) { + if (prevPath == null) { + throw new UnknownIdentifierException(deref.Location, field); + } - DMVariable? property = fromObject.GetVariable(field); - if (property != null) { - operation.Identifier = field; - operation.GlobalId = null; - operation.Path = property.Type; - if (operation.Kind == DMASTDereference.OperationKind.Field && - fromObject.IsSubtypeOf(DreamPath.Client)) { - DMCompiler.Emit(WarningCode.UnsafeClientAccess, deref.Location,"Unsafe \"client\" access. Use the \"?.\" operator instead"); + DMObject? fromObject = DMObjectTree.GetDMObject(prevPath.Value, false); + if (fromObject == null) { + DMCompiler.Emit(WarningCode.ItemDoesntExist, fieldOperation.Location, + $"Type {prevPath.Value} does not exist"); + return new Null(deref.Location); } - } else { - var globalId = fromObject.GetGlobalVariableId(field); - if (globalId != null) { - property = DMObjectTree.Globals[globalId.Value]; - expr = new GlobalField(expr.Location, property.Type, globalId.Value); + property = fromObject.GetVariable(field); + if (!fieldOperation.Safe && fromObject.IsSubtypeOf(DreamPath.Client)) { + DMCompiler.Emit(WarningCode.UnsafeClientAccess, deref.Location, + "Unsafe \"client\" access. Use the \"?.\" operator instead"); + } + + if (property == null && fromObject.GetGlobalVariableId(field) is { } globalId) { + property = DMObjectTree.Globals[globalId]; + + expr = new GlobalField(expr.Location, property.Type, globalId); var newOperationCount = operations.Length - i - 1; if (newOperationCount == 0) { return expr; } + if (property == null) { + throw new UnknownIdentifierException(deref.Location, field); + } + + if ((property.ValType & DMValueType.Unimplemented) == DMValueType.Unimplemented) { + DMCompiler.UnimplementedWarning(deref.Location, + $"{prevPath}.{field} is not implemented and will have unexpected behavior"); + } + operations = new Dereference.Operation[newOperationCount]; astOperationOffset = i + 1; i = -1; + prevPath = property.Type; + pathIsFuzzy = prevPath == null; + continue; } - } - if (property == null) { - throw new UnknownIdentifierException(deref.Location, field); + if (property == null) { + throw new UnknownIdentifierException(deref.Location, field); + } } - if ((property.ValType & DMValueType.Unimplemented) == DMValueType.Unimplemented) { - DMCompiler.UnimplementedWarning(deref.Location, $"{prevPath}.{field} is not implemented and will have unexpected behavior"); - } + operation = new Dereference.FieldOperation { + Safe = fieldOperation.Safe, + Identifier = fieldOperation.Identifier, + Path = property?.Type + }; - prevPath = property.Type; - pathIsFuzzy = false; - } - break; - - case DMASTDereference.OperationKind.FieldSearch: - case DMASTDereference.OperationKind.FieldSafeSearch: - // TODO: im pretty sure types should be inferred if a field with their name only exists in a single place, sounds cursed though - operation.Identifier = astOperation.Identifier.Identifier; - operation.GlobalId = null; - operation.Path = null; - prevPath = null; - pathIsFuzzy = true; + prevPath = property?.Type; + pathIsFuzzy = property == null; break; + } - case DMASTDereference.OperationKind.Index: - case DMASTDereference.OperationKind.IndexSafe: - // Passing the path here is cursed, but one of the tests seems to suggest we want that? - operation.Index = DMExpression.Create(dmObject, proc, astOperation.Index, prevPath); - operation.Path = prevPath; + case DMASTDereference.IndexOperation indexOperation: + operation = new Dereference.IndexOperation { + // Passing the path here is cursed, but one of the tests seems to suggest we want that? + Index = DMExpression.Create(dmObject, proc, indexOperation.Index, prevPath), + Safe = indexOperation.Safe, + Path = prevPath + }; prevPath = null; pathIsFuzzy = true; break; - case DMASTDereference.OperationKind.Call: - case DMASTDereference.OperationKind.CallSafe: { - string field = astOperation.Identifier.Identifier; - ArgumentList argumentList = new(deref.Expression.Location, dmObject, proc, astOperation.Parameters); + case DMASTDereference.CallOperation callOperation: { + var field = callOperation.Identifier; + ArgumentList argumentList = new(deref.Expression.Location, dmObject, proc, callOperation.Parameters); - if (prevPath == null) { - throw new UnknownIdentifierException(deref.Location, field); - } + if (!callOperation.NoSearch && !pathIsFuzzy) { + if (prevPath == null) { + throw new UnknownIdentifierException(deref.Location, field); + } - DMObject? fromObject = DMObjectTree.GetDMObject(prevPath.Value, false); - if (fromObject == null) { - throw new CompileErrorException(deref.Location, $"Type {prevPath.Value} does not exist"); - } + DMObject? fromObject = DMObjectTree.GetDMObject(prevPath.Value, false); + if (fromObject == null) { + DMCompiler.Emit(WarningCode.ItemDoesntExist, callOperation.Location, + $"Type {prevPath.Value} does not exist"); + return new Null(deref.Location); + } - if (!fromObject.HasProc(field)) { - throw new CompileErrorException(deref.Location, $"Type {prevPath.Value} does not have a proc named \"{field}\""); + if (!fromObject.HasProc(field)) { + DMCompiler.Emit(WarningCode.ItemDoesntExist, callOperation.Location, + $"Type {prevPath.Value} does not have a proc named \"{field}\""); + return new Null(deref.Location); + } } - operation.Identifier = astOperation.Identifier.Identifier; - operation.Parameters = argumentList; - operation.Path = null; + operation = new Dereference.CallOperation { + Parameters = argumentList, + Safe = callOperation.Safe, + Identifier = field, + Path = null + }; prevPath = null; pathIsFuzzy = true; break; } - case DMASTDereference.OperationKind.CallSearch: - case DMASTDereference.OperationKind.CallSafeSearch: - operation.Identifier = astOperation.Identifier.Identifier; - operation.Parameters = new ArgumentList(deref.Expression.Location, dmObject, proc, astOperation.Parameters); - operation.Path = null; - prevPath = null; - pathIsFuzzy = true; - break; - default: throw new InvalidOperationException("unhandled deref operation kind"); } + + operations[i] = operation; } // The final value in prevPath is our expression's path! - return new Dereference(deref.Location, prevPath, expr, operations); } From d1e8c7cf032e9b795d620b9d90830d330b3d5763 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 9 Jan 2024 23:58:20 -0500 Subject: [PATCH 44/64] Set `display.windowmode` cvar to 0 in pre-init (#1614) --- OpenDreamClient/EntryPoint.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OpenDreamClient/EntryPoint.cs b/OpenDreamClient/EntryPoint.cs index 1f4fa8484e..59c831e9e6 100644 --- a/OpenDreamClient/EntryPoint.cs +++ b/OpenDreamClient/EntryPoint.cs @@ -27,6 +27,11 @@ public sealed class EntryPoint : GameClient { public override void PreInit() { var config = IoCManager.Resolve(); + + // We share settings with other RT games, such as SS14. + // SS14 supports fullscreen, but it breaks us horribly. This disables fullscreen if it's already set. + config.SetCVar(CVars.DisplayWindowMode, 0); + if (config.GetCVar(OpenDreamCVars.SpoofIEUserAgent)) { config.OverrideDefault(WCVars.WebUserAgentOverride, UserAgent); } From 47dd610e8dab9c79e093812081e388b3c3eea18b Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Wed, 10 Jan 2024 05:24:11 +0000 Subject: [PATCH 45/64] Optimise ProcScheduler.Delays (#1610) * init * a wild priority queue appears * nitpick accepted Co-authored-by: distributivgesetz --------- Co-authored-by: distributivgesetz --- .../Procs/ProcScheduler.Delays.cs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs index fd2033230d..39d1fc3ca3 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Robust.Shared.Collections; using Robust.Shared.Timing; namespace OpenDreamRuntime.Procs; @@ -9,7 +8,7 @@ namespace OpenDreamRuntime.Procs; public sealed partial class ProcScheduler { [Dependency] private readonly IGameTiming _gameTiming = default!; - private ValueList _tickers; + private PriorityQueue _tickers = new(); // This is for deferred tasks that need to fire in the current tick. private readonly Queue _deferredTasks = new(); @@ -53,28 +52,33 @@ public Task CreateDelayTicks(int ticks) { } var tcs = new TaskCompletionSource(); - _tickers.Add(new DelayTicker(tcs) { TicksLeft = ticks + 1 }); // Add 1 because it'll get decreased at the end of this tick + + InsertTask(new DelayTicker(tcs) { TicksAt = _gameTiming.CurTick.Value + (uint)ticks }); //safe cast because ticks is always positive here return tcs.Task; } - private void UpdateDelays() { - // TODO: This is O(n) every tick for the amount of delays we have. - // It may be possible to optimize this. - for (var i = 0; i < _tickers.Count; i++) { - var ticker = _tickers[i]; - ticker.TicksLeft -= 1; - if (ticker.TicksLeft != 0) - continue; + /// + /// Insert a ticker into the queue to maintain sorted order + /// + /// + private void InsertTask(DelayTicker ticker) { + _tickers.Enqueue(ticker, ticker.TicksAt); + } + + private void UpdateDelays() { + while(_tickers.Count > 0) { + var ticker = _tickers.Peek(); + if(ticker.TicksAt > _gameTiming.CurTick.Value) + break; //queue is sorted, so if we hit a ticker that isn't ready, we can stop ticker.TaskCompletionSource.TrySetResult(); - _tickers.RemoveSwap(i); - i -= 1; + _tickers.Dequeue(); } } private sealed class DelayTicker { public readonly TaskCompletionSource TaskCompletionSource; - public int TicksLeft; + public required uint TicksAt; public DelayTicker(TaskCompletionSource taskCompletionSource) { TaskCompletionSource = taskCompletionSource; From 816e59c493ef05794a9734d950cb421cf07fd967 Mon Sep 17 00:00:00 2001 From: Hinaichigo Date: Wed, 10 Jan 2024 11:00:52 -0800 Subject: [PATCH 46/64] Update _Standard.dm (#1619) stub get_steps_to() --- DMCompiler/DMStandard/_Standard.dm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index 8c730da05b..b7fe54be28 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -162,6 +162,10 @@ proc/replacetextEx_char(Haystack, Needle, Replacement, Start = 1, End = 0) set opendream_unimplemented = TRUE CRASH("/get_step_to() is not implemented") +/proc/get_steps_to(Ref, Trg, Min=0) + set opendream_unimplemented = TRUE + CRASH("/get_steps_to() is not implemented") + /proc/walk_away(Ref,Trg,Max=5,Lag=0,Speed=0) set opendream_unimplemented = TRUE CRASH("/walk_away() is not implemented") From 5769beafdd4b2061d20f09bc819dbb8ac49f6072 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 10 Jan 2024 15:14:03 -0500 Subject: [PATCH 47/64] Handle `glide_size` being updated mid-glide (#1616) --- OpenDreamClient/Rendering/AtomGlideSystem.cs | 20 +++++++++++++------- OpenDreamRuntime/AtomManager.cs | 2 +- OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 3 +-- OpenDreamShared/Dream/IconAppearance.cs | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/OpenDreamClient/Rendering/AtomGlideSystem.cs b/OpenDreamClient/Rendering/AtomGlideSystem.cs index a0b072132a..72a3ba8edb 100644 --- a/OpenDreamClient/Rendering/AtomGlideSystem.cs +++ b/OpenDreamClient/Rendering/AtomGlideSystem.cs @@ -8,10 +8,10 @@ namespace OpenDreamClient.Rendering; /// Disables RobustToolbox's transform lerping and replaces it with our own gliding /// public sealed class AtomGlideSystem : EntitySystem { - private sealed class Glide(TransformComponent transform) { + private sealed class Glide(TransformComponent transform, DMISpriteComponent sprite) { public readonly TransformComponent Transform = transform; + public readonly DMISpriteComponent Sprite = sprite; public Vector2 EndPos; - public float MovementSpeed; } [Dependency] private readonly TransformSystem _transformSystem = default!; @@ -42,9 +42,16 @@ public override void FrameUpdate(float frameTime) { for (int i = 0; i < _currentGlides.Count; i++) { var glide = _currentGlides[i]; + + if (glide.Sprite.Icon.Appearance == null) { + _currentGlides.RemoveSwap(i--); + continue; + } + var currentPos = glide.Transform.LocalPosition; var newPos = currentPos; - var movement = glide.MovementSpeed * frameTime; + var movementSpeed = CalculateMovementSpeed(glide.Sprite.Icon.Appearance.GlideSize); + var movement = movementSpeed * frameTime; // Move X towards the end position at a constant speed if (!MathHelper.CloseTo(currentPos.X, glide.EndPos.X)) { @@ -111,7 +118,7 @@ private void OnTransformMove(ref MoveEvent e) { } if (glide == null) { - glide = new(e.Component); + glide = new(e.Component, sprite); _currentGlides.Add(glide); } @@ -120,16 +127,15 @@ private void OnTransformMove(ref MoveEvent e) { _transformSystem.SetLocalPositionNoLerp(e.Sender, startingFrom, e.Component); glide.EndPos = glidingTo; - glide.MovementSpeed = CalculateMovementSpeed(sprite.Icon.Appearance.GlideSize); _ignoreMoveEvent = false; } - private static float CalculateMovementSpeed(byte glideSize) { + private static float CalculateMovementSpeed(float glideSize) { if (glideSize == 0) glideSize = 4; // TODO: 0 gives us "automated control" over this value, not just setting it to 4 // Assume a 20 TPS server // TODO: Support other TPS - return (float)glideSize / EyeManager.PixelsPerMeter * 20f; + return glideSize / EyeManager.PixelsPerMeter * 20f; } } diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index cdc5d03c2a..35a8dd3bad 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -328,7 +328,7 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal break; case "glide_size": value.TryGetValueAsFloat(out float glideSize); - appearance.GlideSize = (byte) glideSize; + appearance.GlideSize = glideSize; break; case "render_source": value.TryGetValueAsString(out appearance.RenderSource); diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 40a5f7049f..ecd3ddce4c 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1697,14 +1697,13 @@ public static ProcStatus SwitchCaseRange(DMProcState state) { public static ProcStatus Spawn(DMProcState state) { int jumpTo = state.ReadInt(); state.Pop().TryGetValueAsFloat(out var delay); - int delayMilliseconds = (int)(delay * 100); // TODO: It'd be nicer if we could use something such as DreamThread.Spawn here // and have state.Spawn return a ProcState instead DreamThread newContext = state.Spawn(); //Negative delays mean the spawned code runs immediately - if (delayMilliseconds < 0) { + if (delay < 0) { newContext.Resume(); // TODO: Does the rest of the proc get scheduled? // Does the value of the delay mean anything? diff --git a/OpenDreamShared/Dream/IconAppearance.cs b/OpenDreamShared/Dream/IconAppearance.cs index e82c044d24..7847f6d742 100644 --- a/OpenDreamShared/Dream/IconAppearance.cs +++ b/OpenDreamShared/Dream/IconAppearance.cs @@ -17,7 +17,7 @@ public sealed class IconAppearance : IEquatable { [ViewVariables] public Vector2i PixelOffset; [ViewVariables] public Color Color = Color.White; [ViewVariables] public byte Alpha = 255; - [ViewVariables] public byte GlideSize; + [ViewVariables] public float GlideSize; /// /// An appearance can gain a color matrix filter by two possible forces:
/// 1. the /atom.color var is modified.
From 6ab43559e51f968c66e16d00f5d89a75c6ec9e93 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 10 Jan 2024 15:45:12 -0500 Subject: [PATCH 48/64] Optimize `DMISpriteComponent.IsVisible()` (#1615) --- .../Rendering/DMISpriteComponent.cs | 18 ++++++++++-------- OpenDreamClient/Rendering/DreamViewOverlay.cs | 14 +++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/OpenDreamClient/Rendering/DMISpriteComponent.cs b/OpenDreamClient/Rendering/DMISpriteComponent.cs index f2ea0f43b6..a852e12587 100644 --- a/OpenDreamClient/Rendering/DMISpriteComponent.cs +++ b/OpenDreamClient/Rendering/DMISpriteComponent.cs @@ -8,17 +8,19 @@ internal sealed partial class DMISpriteComponent : SharedDMISpriteComponent { [ViewVariables] public DreamIcon Icon { get; set; } [ViewVariables] public ScreenLocation? ScreenLocation { get; set; } - [Dependency] private readonly IEntityManager _entityManager = default!; + /// + /// Checks if this sprite should be visible to the player
+ /// Checks the appearance's invisibility, and if a transform is given, whether it's parented to another entity + ///
+ /// The entity's transform, the parent check is skipped if this is null + /// The eye's see_invisibility var + /// + public bool IsVisible(TransformComponent? transform, int seeInvisibility) { + if (Icon.Appearance?.Invisibility > seeInvisibility) return false; - public bool IsVisible(bool checkWorld = true, int seeInvis = 0) { - if (Icon.Appearance?.Invisibility > seeInvis) return false; - - if (checkWorld) { + if (transform != null) { //Only render movables not inside another movable's contents (parented to the grid) //TODO: Use RobustToolbox's container system/components? - if (!_entityManager.TryGetComponent(Owner, out var transform)) - return false; - if (transform.ParentUid != transform.GridUid) return false; } diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 4f3cd00db2..785437080b 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -752,12 +752,14 @@ private void DrawPlanes(DrawingHandleWorld handle) { // TODO use a sprite tree. if (!_spriteQuery.TryGetComponent(entity, out var sprite)) continue; - if (!sprite.IsVisible(seeInvis: seeVis)) + + var transform = _xformQuery.GetComponent(entity); + if (!sprite.IsVisible(transform, seeVis)) continue; if (sprite.Icon.Appearance == null) //appearance hasn't loaded yet continue; - var worldPos = _transformSystem.GetWorldPosition(entity, _xformQuery); + var worldPos = _transformSystem.GetWorldPosition(transform); var tilePos = grid.WorldToTile(worldPos) - eyeTile.GridIndices + viewRange.Center; if (tilePos.X < 0 || tilePos.Y < 0 || tilePos.X >= _tileInfo.GetLength(0) || tilePos.Y >= _tileInfo.GetLength(1)) continue; @@ -801,10 +803,12 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, MapGridComponen // TODO use a sprite tree. if (!_spriteQuery.TryGetComponent(entity, out var sprite)) continue; - if (!sprite.IsVisible(seeInvis: seeVis)) + + var transform = _xformQuery.GetComponent(entity); + if (!sprite.IsVisible(transform, seeVis)) continue; - var worldPos = _transformSystem.GetWorldPosition(entity, _xformQuery); + var worldPos = _transformSystem.GetWorldPosition(transform); // Check for visibility if the eye doesn't have SEE_OBJS or SEE_MOBS // TODO: Differentiate between objs and mobs @@ -830,7 +834,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, MapGridComponen foreach (EntityUid uid in _screenOverlaySystem.ScreenObjects) { if (!_entityManager.TryGetComponent(uid, out DMISpriteComponent? sprite) || sprite.ScreenLocation == null) continue; - if (!sprite.IsVisible(checkWorld: false, seeInvis: seeVis)) + if (!sprite.IsVisible(null, seeVis)) continue; if (sprite.ScreenLocation.MapControl != null) // Don't render screen objects meant for other map controls continue; From 70774e4843459f5ab9184434575f1b2b74f8e93d Mon Sep 17 00:00:00 2001 From: ike709 Date: Wed, 10 Jan 2024 17:51:42 -0700 Subject: [PATCH 49/64] Fix empty `for()` loop body error location (#1592) Co-authored-by: ike709 Co-authored-by: wixoa --- DMCompiler/Compiler/DM/DMParser.cs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index a4bc611c20..2bad848edb 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -1047,7 +1047,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { if (body == null) { DMASTProcStatement? statement = ProcStatement(); - if (statement == null) Error("Expected body or statement"); + if (statement == null) Error(WarningCode.BadExpression, "Expected body or statement"); body = new DMASTProcBlockInner(loc, statement); } @@ -1124,7 +1124,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { Whitespace(); if (Check(TokenType.DM_RightParenthesis)) { - return new DMASTProcStatementInfLoop(loc, GetForBody()); + return new DMASTProcStatementInfLoop(loc, GetForBody(loc)); } _allowVarDeclExpression = true; @@ -1144,7 +1144,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { if (expr1 is DMASTAssign assign) { ExpressionTo(out var endRange, out var step); Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after to expression"); - return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.Expression, assign.Value, endRange, step), null, null, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.Expression, assign.Value, endRange, step), null, null, dmTypes, GetForBody(loc)); } else { Error("Expected = before to in for"); } @@ -1155,16 +1155,16 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { DMASTExpression? listExpr = Expression(); Whitespace(); Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2"); - return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody(loc)); } if (!Check(ForSeparatorTypes)) { Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 1"); - return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc)); } if (Check(TokenType.DM_RightParenthesis)) { - return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc)); } Whitespace(); @@ -1179,11 +1179,11 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { if (!Check(ForSeparatorTypes)) { Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2"); - return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc)); } if (Check(TokenType.DM_RightParenthesis)) { - return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc)); } Whitespace(); @@ -1197,25 +1197,27 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { } Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 3"); - return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody()); + return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody(loc)); } return null; - DMASTProcBlockInner GetForBody() { + DMASTProcBlockInner GetForBody(Location forLocation) { Whitespace(); Newline(); DMASTProcBlockInner? body = ProcBlock(); if (body == null) { var loc = Current().Location; - DMASTProcStatement? statement; if (Check(TokenType.DM_Semicolon)) { statement = new DMASTProcStatementExpression(loc, new DMASTConstantNull(loc)); } else { statement = ProcStatement(); - if (statement == null) Error("Expected body or statement"); + if (statement == null) { + DMCompiler.Emit(WarningCode.BadExpression, forLocation, "Expected body or statement"); + statement = new DMASTProcStatementExpression(loc, new DMASTConstantNull(loc)); // just so we can continue compiling. + } } body = new DMASTProcBlockInner(loc, statement); } @@ -1453,7 +1455,7 @@ public DMASTProcStatementSwitch.SwitchCase[] SwitchInner() { if (tryBody == null) { DMASTProcStatement? statement = ProcStatement(); - if (statement == null) Error("Expected body or statement"); + if (statement == null) Error(WarningCode.BadExpression, "Expected body or statement"); tryBody = new DMASTProcBlockInner(loc,statement); } From d169f4cc97d1af9a594ae9cbde58bdf8d6f48f06 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Tue, 16 Jan 2024 04:00:39 +0000 Subject: [PATCH 50/64] Tiny fix to enumerators (#1612) --- OpenDreamRuntime/Procs/DreamEnumerators.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamRuntime/Procs/DreamEnumerators.cs b/OpenDreamRuntime/Procs/DreamEnumerators.cs index 146bd373de..b954ea507b 100644 --- a/OpenDreamRuntime/Procs/DreamEnumerators.cs +++ b/OpenDreamRuntime/Procs/DreamEnumerators.cs @@ -55,7 +55,7 @@ public bool Enumerate(DMProcState state, DreamReference? reference) { // Assign regardless of success if (reference != null) - state.AssignReference(reference.Value, new DreamValue(_dreamObjectEnumerator.Current)); + state.AssignReference(reference.Value, success ? new DreamValue(_dreamObjectEnumerator.Current) : DreamValue.Null); return success; } } From 545e3afe2e0a018587b7d6a8da7ba7a956359807 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 17 Jan 2024 01:18:13 -0500 Subject: [PATCH 51/64] Halve the renderer's allocation rate (#1621) --- OpenDreamClient/DreamClientSystem.cs | 3 +- OpenDreamClient/Rendering/DreamPlane.cs | 52 ++- OpenDreamClient/Rendering/DreamViewOverlay.cs | 434 ++++++++---------- 3 files changed, 240 insertions(+), 249 deletions(-) diff --git a/OpenDreamClient/DreamClientSystem.cs b/OpenDreamClient/DreamClientSystem.cs index 029c7ca44b..341cfa8a05 100644 --- a/OpenDreamClient/DreamClientSystem.cs +++ b/OpenDreamClient/DreamClientSystem.cs @@ -10,6 +10,7 @@ internal sealed class DreamClientSystem : EntitySystem { [Dependency] private readonly IDreamInterfaceManager _interfaceManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly MapSystem _mapSystem = default!; [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly ClientScreenOverlaySystem _screenOverlaySystem = default!; @@ -18,7 +19,7 @@ internal sealed class DreamClientSystem : EntitySystem { public override void Initialize() { SubscribeLocalEvent(OnPlayerAttached); - var mapOverlay = new DreamViewOverlay(_transformSystem, _lookupSystem, _appearanceSystem, _screenOverlaySystem, _clientImagesSystem); + var mapOverlay = new DreamViewOverlay(_transformSystem, _mapSystem, _lookupSystem, _appearanceSystem, _screenOverlaySystem, _clientImagesSystem); _overlayManager.AddOverlay(mapOverlay); } diff --git a/OpenDreamClient/Rendering/DreamPlane.cs b/OpenDreamClient/Rendering/DreamPlane.cs index 1932e4d6c5..74bc93f99c 100644 --- a/OpenDreamClient/Rendering/DreamPlane.cs +++ b/OpenDreamClient/Rendering/DreamPlane.cs @@ -1,4 +1,5 @@ -using Robust.Client.Graphics; +using OpenDreamShared.Dream; +using Robust.Client.Graphics; using Robust.Shared.Utility; namespace OpenDreamClient.Rendering; @@ -7,15 +8,13 @@ internal sealed class DreamPlane(IRenderTexture mainRenderTarget) : IDisposable public IRenderTexture RenderTarget => _temporaryRenderTarget ?? mainRenderTarget; public RendererMetaData? Master; - public readonly List> IconDrawActions = new(); - public readonly List> MouseMapDrawActions = new(); + public readonly List Sprites = new(); private IRenderTexture? _temporaryRenderTarget; public void Clear() { Master = null; - IconDrawActions.Clear(); - MouseMapDrawActions.Clear(); + Sprites.Clear(); _temporaryRenderTarget = null; } @@ -44,21 +43,25 @@ public void SetTemporaryRenderTarget(IRenderTexture renderTarget) { /// /// Clears this plane's render target, then draws all the plane's icons onto it /// - public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle) { + public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle, Box2 worldAABB) { // Draw all icons handle.RenderInRenderTarget(mainRenderTarget, () => { - foreach (Action iconAction in IconDrawActions) - iconAction(mainRenderTarget.Size); + foreach (var sprite in Sprites) { + if (sprite.HasRenderSource && overlay.RenderSourceLookup.TryGetValue(sprite.RenderSource!, out var renderSourceTexture)) { + sprite.TextureOverride = renderSourceTexture.Texture; + overlay.DrawIcon(handle, mainRenderTarget.Size, sprite, (-worldAABB.BottomLeft)-(worldAABB.Size/2)+new Vector2(0.5f,0.5f)); + } else { + overlay.DrawIcon(handle, mainRenderTarget.Size, sprite, -worldAABB.BottomLeft); + } + } }, new Color()); if (_temporaryRenderTarget != null) { // Draw again, but with the color applied handle.RenderInRenderTarget(_temporaryRenderTarget, () => { handle.UseShader(overlay.GetBlendAndColorShader(Master, useOverlayMode: true)); - handle.SetTransform(overlay.CreateRenderTargetFlipMatrix(_temporaryRenderTarget.Size, Vector2.Zero)); + handle.SetTransform(DreamViewOverlay.CreateRenderTargetFlipMatrix(_temporaryRenderTarget.Size, Vector2.Zero)); handle.DrawTextureRect(mainRenderTarget.Texture, new Box2(Vector2.Zero, mainRenderTarget.Size)); - handle.SetTransform(Matrix3.Identity); - handle.UseShader(null); }, new Color()); } } @@ -66,8 +69,29 @@ public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle) { /// /// Draws this plane's mouse map onto the current render target /// - public void DrawMouseMap(Vector2i renderTargetSize) { - foreach (Action mouseMapAction in MouseMapDrawActions) - mouseMapAction(renderTargetSize); + public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Vector2i renderTargetSize, Box2 worldAABB) { + handle.UseShader(overlay.BlockColorInstance); + foreach (var sprite in Sprites) { + if (sprite.MouseOpacity == MouseOpacity.Transparent || sprite.ShouldPassMouse) + continue; + + var texture = sprite.Texture; + if (texture == null) + continue; + + var pos = (sprite.Position - worldAABB.BottomLeft) * EyeManager.PixelsPerMeter; + if (sprite.TextureOverride != null) + pos -= sprite.TextureOverride.Size / 2 - new Vector2(EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter) / 2; + + int hash = sprite.GetHashCode(); + var colorR = (byte)(hash & 0xFF); + var colorG = (byte)((hash >> 8) & 0xFF); + var colorB = (byte)((hash >> 16) & 0xFF); + Color targetColor = new Color(colorR, colorG, colorB); //TODO - this could result in mis-clicks due to hash-collision since we ditch a whole byte. + overlay.MouseMapLookup[targetColor] = sprite; + + handle.SetTransform(DreamViewOverlay.CreateRenderTargetFlipMatrix(renderTargetSize, pos)); + handle.DrawTextureRect(texture, new Box2(Vector2.Zero, texture.Size), targetColor); + } } } diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 785437080b..9667345e1c 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Runtime.CompilerServices; using OpenDreamClient.Interface; using Robust.Client.Graphics; using Robust.Client.Player; @@ -12,6 +13,7 @@ using Robust.Shared.Map.Components; using Robust.Shared.Profiling; using Vector3 = Robust.Shared.Maths.Vector3; +using Dependency = Robust.Shared.IoC.DependencyAttribute; namespace OpenDreamClient.Rendering; @@ -24,8 +26,10 @@ internal sealed class DreamViewOverlay : Overlay { public bool ScreenOverlayEnabled = true; public bool MouseMapRenderEnabled; + public ShaderInstance BlockColorInstance; public Texture? MouseMap => _mouseMapRenderTarget?.Texture; public readonly Dictionary MouseMapLookup = new(); + public readonly Dictionary RenderSourceLookup = new(); private const LookupFlags MapLookupFlags = LookupFlags.Approximate | LookupFlags.Uncontained; @@ -40,6 +44,7 @@ internal sealed class DreamViewOverlay : Overlay { private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.view"); private readonly TransformSystem _transformSystem; + private readonly MapSystem _mapSystem; private readonly EntityLookupSystem _lookupSystem; private readonly ClientAppearanceSystem _appearanceSystem; private readonly ClientScreenOverlaySystem _screenOverlaySystem; @@ -52,28 +57,30 @@ internal sealed class DreamViewOverlay : Overlay { private readonly Dictionary _planes = new(); private readonly List _spriteContainer = new(); - private readonly ShaderInstance _blockColorInstance; - private readonly ShaderInstance _colorInstance; private readonly Dictionary _blendModeInstances; + private static ShaderInstance _colorInstance = default!; private readonly Dictionary> _renderTargetCache = new(); private IRenderTexture? _mouseMapRenderTarget; private IRenderTexture? _baseRenderTarget; - private readonly Dictionary _renderSourceLookup = new(); private readonly Stack _renderTargetsToReturn = new(); private readonly Stack _rendererMetaDataRental = new(); private readonly Stack _rendererMetaDataToReturn = new(); - private readonly Matrix3 _flipMatrix; + + private static readonly Matrix3 FlipMatrix = Matrix3.Identity with { + R1C1 = -1 + }; // Defined here so it isn't recreated every frame private ViewAlgorithm.Tile?[,]? _tileInfo; private readonly HashSet _entities = new(); - public DreamViewOverlay(TransformSystem transformSystem, EntityLookupSystem lookupSystem, + public DreamViewOverlay(TransformSystem transformSystem, MapSystem mapSystem, EntityLookupSystem lookupSystem, ClientAppearanceSystem appearanceSystem, ClientScreenOverlaySystem screenOverlaySystem, ClientImagesSystem clientImagesSystem) { IoCManager.InjectDependencies(this); _transformSystem = transformSystem; + _mapSystem = mapSystem; _lookupSystem = lookupSystem; _appearanceSystem = appearanceSystem; _screenOverlaySystem = screenOverlaySystem; @@ -84,7 +91,7 @@ public DreamViewOverlay(TransformSystem transformSystem, EntityLookupSystem look _mobSightQuery = _entityManager.GetEntityQuery(); _sawmill.Debug("Loading shaders..."); - _blockColorInstance = _protoManager.Index("blockcolor").InstanceUnique(); + BlockColorInstance = _protoManager.Index("blockcolor").InstanceUnique(); _colorInstance = _protoManager.Index("color").InstanceUnique(); _blendModeInstances = new(6) { {BlendMode.Default, _protoManager.Index("blend_overlay").InstanceUnique()}, //BLEND_DEFAULT (Same as BLEND_OVERLAY when there's no parent) @@ -94,15 +101,12 @@ public DreamViewOverlay(TransformSystem transformSystem, EntityLookupSystem look {BlendMode.Multiply, _protoManager.Index("blend_multiply").InstanceUnique()}, //BLEND_MULTIPLY {BlendMode.InsertOverlay, _protoManager.Index("blend_inset_overlay").InstanceUnique()} //BLEND_INSET_OVERLAY //TODO }; - - _flipMatrix = Matrix3.Identity; - _flipMatrix.R1C1 = -1; } protected override void Draw(in OverlayDrawArgs args) { using var _ = _prof.Group("Dream View Overlay"); - EntityUid? eye = _playerManager.LocalPlayer?.Session.AttachedEntity; + EntityUid? eye = _playerManager.LocalSession?.AttachedEntity; if (eye == null) return; @@ -118,7 +122,7 @@ protected override void Draw(in OverlayDrawArgs args) { _appearanceSystem.CleanUpUnusedFilters(); _appearanceSystem.ResetFilterUsageFlags(); - _renderSourceLookup.Clear(); + RenderSourceLookup.Clear(); //some render targets need to be kept until the end of the render cycle, so return them here. while(_renderTargetsToReturn.Count > 0) @@ -132,7 +136,9 @@ protected override void Draw(in OverlayDrawArgs args) { private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) { if (!_xformQuery.TryGetComponent(eye, out var eyeTransform)) return; - if (!_mapManager.TryFindGridAt(eyeTransform.MapPosition, out _, out var grid)) + + var eyeCoords = _transformSystem.GetMapCoordinates(eye, eyeTransform); + if (!_mapManager.TryFindGridAt(eyeCoords, out var gridUid, out var grid)) return; _mobSightQuery.TryGetComponent(eye, out var mobSight); @@ -147,17 +153,17 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) _lookupSystem.GetEntitiesIntersecting(args.MapId, args.WorldAABB.Scale(1.2f), _entities, MapLookupFlags); } - var eyeTile = grid.GetTileRef(eyeTransform.MapPosition); - var tiles = CalculateTileVisibility(grid, _entities, eyeTile, seeVis); + var eyeTile = _mapSystem.GetTileRef(gridUid, grid, eyeCoords); + var tiles = CalculateTileVisibility(gridUid, grid, _entities, eyeTile, seeVis); RefreshRenderTargets(args.WorldHandle, viewportSize); - CollectVisibleSprites(tiles, grid, eyeTile, _entities, seeVis, sight, args.WorldAABB); + CollectVisibleSprites(tiles, gridUid, grid, eyeTile, _entities, seeVis, sight, args.WorldAABB); ClearPlanes(); ProcessSprites(worldHandle, viewportSize, args.WorldAABB); //Final draw - DrawPlanes(worldHandle); + DrawPlanes(worldHandle, args.WorldAABB); //At this point all the sprites have been rendered to the base target, now we just draw it to the viewport! worldHandle.DrawTexture( @@ -215,10 +221,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u else current.Plane = icon.Appearance.Plane; - if (icon.Appearance.Layer < 0) //FLOAT_LAYER - current.Layer = parentIcon.Layer; - else - current.Layer = icon.Appearance.Layer; + current.Layer = (icon.Appearance.Layer < 0) ? parentIcon.Layer : icon.Appearance.Layer; //FLOAT_LAYER if (current.BlendMode == BlendMode.Default) current.BlendMode = parentIcon.BlendMode; @@ -264,7 +267,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u renderTargetPlaceholder.BlendMode = current.BlendMode; } renderTargetPlaceholder.AppearanceFlags = current.AppearanceFlags; - current.AppearanceFlags = current.AppearanceFlags & ~AppearanceFlags.PlaneMaster; //only the placeholder should be marked as master + current.AppearanceFlags &= ~AppearanceFlags.PlaneMaster; //only the placeholder should be marked as master result.Add(renderTargetPlaceholder); } @@ -305,8 +308,8 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u //notably they cannot be applied to overlays, so don't check for them if this is an under/overlay //note also that we use turfCoords and not current.Position because we want world-coordinates, not screen coordinates. This is only used for turfs. if(parentIcon == null && _clientImagesSystem.TryGetClientImages(current.Uid, turfCoords, out List? attachedClientImages)){ - foreach(NetEntity CINetEntity in attachedClientImages){ - EntityUid imageEntity = _entityManager.GetEntity(CINetEntity); + foreach(NetEntity ciNetEntity in attachedClientImages) { + EntityUid imageEntity = _entityManager.GetEntity(ciNetEntity); if (!_spriteQuery.TryGetComponent(imageEntity, out var sprite)) continue; if(sprite.Icon.Appearance == null) @@ -314,7 +317,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u if(sprite.Icon.Appearance.Override) current.MainIcon = sprite.Icon; else - ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, current, false); + ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, current); } } @@ -334,7 +337,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u //TODO particles - colour and transform don't apply? //flatten KeepTogetherGroup. Done here so we get implicit recursive iteration down the tree. - if (current.KeepTogetherGroup != null && current.KeepTogetherGroup.Count > 0) { + if (current.KeepTogetherGroup?.Count > 0) { List flatKeepTogetherGroup = new List(current.KeepTogetherGroup.Count); foreach (RendererMetaData ktItem in current.KeepTogetherGroup) { @@ -369,8 +372,10 @@ private IRenderTexture RentRenderTarget(Vector2i size) { } private void ReturnRenderTarget(IRenderTexture rental) { - if (!_renderTargetCache.TryGetValue(rental.Size, out var storeList)) + if (!_renderTargetCache.TryGetValue(rental.Size, out var storeList)) { storeList = new List(4); + _renderTargetCache.Add(rental.Size, storeList); + } storeList.Add(rental); } @@ -395,97 +400,36 @@ private void ClearRenderTarget(IRenderTexture target, DrawingHandleWorld handle, colorMatrix.Equals(ColorMatrix.Identity)) return null; - if (!_blendModeInstances.TryGetValue(blendMode, out var blendAndColor)) - blendAndColor = _blendModeInstances[BlendMode.Default]; - - blendAndColor = blendAndColor.Duplicate(); + var blendAndColor = _blendModeInstances[blendMode].Duplicate(); blendAndColor.SetParameter("colorMatrix", colorMatrix.GetMatrix4()); blendAndColor.SetParameter("offsetVector", colorMatrix.GetOffsetVector()); blendAndColor.SetParameter("isPlaneMaster", iconMetaData.IsPlaneMaster); return blendAndColor; } - private (Action?, Action?) DrawIconAction(DrawingHandleWorld handle, RendererMetaData iconMetaData, Vector2 positionOffset, Texture? textureOverride = null) { + public void DrawIcon(DrawingHandleWorld handle, Vector2i renderTargetSize, RendererMetaData iconMetaData, Vector2 positionOffset) { DreamIcon? icon = iconMetaData.MainIcon; if (icon == null) - return (null, null); - - Vector2 position = iconMetaData.Position + positionOffset; - Vector2 pixelPosition = position*EyeManager.PixelsPerMeter; - - Texture? frame; - if (textureOverride != null) { - frame = textureOverride; - } else { - frame = icon.CurrentFrame; - } + return; //KEEP_TOGETHER groups - if (iconMetaData.KeepTogetherGroup != null && iconMetaData.KeepTogetherGroup.Count > 0) { - //store the parent's transform, color, blend, and alpha - then clear them for drawing to the render target - Matrix3 ktParentTransform = iconMetaData.TransformToApply; - Color ktParentColor = iconMetaData.ColorToApply; - float ktParentAlpha = iconMetaData.AlphaToApply; - BlendMode ktParentBlendMode = iconMetaData.BlendMode; - - iconMetaData.TransformToApply = Matrix3.Identity; - iconMetaData.ColorToApply = Color.White; - iconMetaData.AlphaToApply = 1f; - iconMetaData.BlendMode = BlendMode.Default; - - List ktItems = new List(iconMetaData.KeepTogetherGroup.Count+1); - ktItems.Add(iconMetaData); - ktItems.AddRange(iconMetaData.KeepTogetherGroup); - iconMetaData.KeepTogetherGroup.Clear(); - - ktItems.Sort(); - //draw it onto an additional render target that we can return immediately for correction of transform + if (iconMetaData.KeepTogetherGroup?.Count > 0) { // TODO: Use something better than a hardcoded 64x64 fallback - IRenderTexture tempTexture = RentRenderTarget(frame?.Size ?? (64,64)); - ClearRenderTarget(tempTexture, handle, Color.Transparent); - - foreach (RendererMetaData ktItem in ktItems) { - DrawIconNow(handle, tempTexture.Size, tempTexture, ktItem, -ktItem.Position); - } - - //but keep the handle to the final KT group's render target so we don't override it later in the render cycle - IRenderTexture ktTexture = RentRenderTarget(tempTexture.Size); - handle.RenderInRenderTarget(ktTexture, () => { - handle.SetTransform(CreateRenderTargetFlipMatrix(tempTexture.Size, Vector2.Zero)); - handle.DrawTextureRect(tempTexture.Texture, new Box2(Vector2.Zero, tempTexture.Size)); - handle.SetTransform(Matrix3.Identity); - }, Color.Transparent); - - frame = ktTexture.Texture; - _renderTargetsToReturn.Push(tempTexture); - - //now restore the original color, alpha, blend, and transform so they can be applied to the render target as a whole - iconMetaData.TransformToApply = ktParentTransform; - iconMetaData.ColorToApply = ktParentColor; - iconMetaData.AlphaToApply = ktParentAlpha; - iconMetaData.BlendMode = ktParentBlendMode; - - _renderTargetsToReturn.Push(ktTexture); + iconMetaData.TextureOverride = ProcessKeepTogether(handle, iconMetaData, iconMetaData.Texture?.Size ?? (64,64)); } - //if frame is still null, this doesn't require a draw, so return NOP - if (frame == null) - return (null, null); + var pixelPosition = (iconMetaData.Position + positionOffset) * EyeManager.PixelsPerMeter; + var frame = iconMetaData.Texture; - Action iconDrawAction; - Action? mouseMapDrawAction; - - //setup the MouseMapLookup shader for use in DrawIcon() - int hash = iconMetaData.GetHashCode(); - var colorR = (byte)(hash & 0xFF); - var colorG = (byte)((hash >> 8) & 0xFF); - var colorB = (byte)((hash >> 16) & 0xFF); - Color targetColor = new Color(colorR, colorG, colorB); //TODO - this could result in mis-clicks due to hash-collision since we ditch a whole byte. - MouseMapLookup[targetColor] = iconMetaData; + //if frame is null, this doesn't require a draw, so return NOP + if (frame == null) + return; //go fast when the only filter is color, and we don't have more color things to consider bool goFastOverride = false; - if (icon.Appearance != null && iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity) && iconMetaData.ColorToApply == Color.White && iconMetaData.AlphaToApply == 1.0f && icon.Appearance.Filters.Count == 1 && icon.Appearance.Filters[0].FilterType == "color") { + if (icon.Appearance != null && iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity) && + iconMetaData.ColorToApply == Color.White && iconMetaData.AlphaToApply.Equals(1.0f) && + icon.Appearance.Filters is [{ FilterType: "color" }]) { DreamFilterColor colorFilter = (DreamFilterColor)icon.Appearance.Filters[0]; iconMetaData.ColorMatrixToApply = colorFilter.Color; goFastOverride = true; @@ -493,93 +437,10 @@ private void ClearRenderTarget(IRenderTexture target, DrawingHandleWorld handle, if (goFastOverride || icon.Appearance == null || icon.Appearance.Filters.Count == 0) { //faster path for rendering unfiltered sprites - iconDrawAction = renderTargetSize => { - handle.UseShader(GetBlendAndColorShader(iconMetaData)); - handle.SetTransform(CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition)); - handle.DrawTextureRect(frame, Box2.FromDimensions(Vector2.Zero, frame.Size)); - handle.UseShader(null); - handle.SetTransform(Matrix3.Identity); - }; - - if (iconMetaData.MouseOpacity != MouseOpacity.Transparent && !iconMetaData.ShouldPassMouse) { - mouseMapDrawAction = renderTargetSize => { - handle.UseShader(_blockColorInstance); - handle.SetTransform(CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition)); - handle.DrawTextureRect(frame, new Box2(Vector2.Zero, frame.Size), targetColor); - handle.SetTransform(Matrix3.Identity); - handle.UseShader(null); - }; - } else { - mouseMapDrawAction = null; - } - - return (iconDrawAction, mouseMapDrawAction); - } else { //Slower path for filtered icons - //first we do ping pong rendering for the multiple filters - // TODO: This should determine the size from the filters and their settings, not just double the original - IRenderTexture ping = RentRenderTarget(frame.Size * 2); - IRenderTexture pong = RentRenderTarget(frame.Size * 2); - - handle.RenderInRenderTarget(pong, () => { - //we can use the color matrix shader here, since we don't need to blend - //also because blend mode is none, we don't need to clear - ColorMatrix colorMatrix; - if (iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity)) - colorMatrix = new ColorMatrix(iconMetaData.ColorToApply.WithAlpha(iconMetaData.AlphaToApply)); - else - colorMatrix = iconMetaData.ColorMatrixToApply; - - ShaderInstance colorShader = _colorInstance.Duplicate(); - colorShader.SetParameter("colorMatrix", colorMatrix.GetMatrix4()); - colorShader.SetParameter("offsetVector", colorMatrix.GetOffsetVector()); - colorShader.SetParameter("isPlaneMaster",iconMetaData.IsPlaneMaster); - handle.UseShader(colorShader); - - handle.SetTransform(CreateRenderTargetFlipMatrix(pong.Size, frame.Size / 2)); - handle.DrawTextureRect(frame, new Box2(Vector2.Zero, frame.Size)); - handle.SetTransform(Matrix3.Identity); - handle.UseShader(null); - }, Color.Black.WithAlpha(0)); - - foreach (DreamFilter filterId in icon.Appearance.Filters) { - ShaderInstance s = _appearanceSystem.GetFilterShader(filterId, _renderSourceLookup); - - handle.RenderInRenderTarget(ping, () => { - handle.UseShader(s); - handle.SetTransform(CreateRenderTargetFlipMatrix(ping.Size, Vector2.Zero)); - handle.DrawTextureRect(pong.Texture, new Box2(Vector2.Zero, pong.Size)); - handle.SetTransform(Matrix3.Identity); - handle.UseShader(null); - }, Color.Black.WithAlpha(0)); - - (ping, pong) = (pong, ping); - } - - //then we return the Action that draws the actual icon with filters applied - iconDrawAction = renderTargetSize => { - //note we apply the color *before* the filters, so we use override here - handle.UseShader(GetBlendAndColorShader(iconMetaData, ignoreColor: true)); - handle.SetTransform(CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition - frame.Size / 2)); - handle.DrawTextureRect(pong.Texture, new Box2(Vector2.Zero, pong.Size)); - handle.UseShader(null); - handle.SetTransform(Matrix3.Identity); - }; - - if (iconMetaData.MouseOpacity != MouseOpacity.Transparent && !iconMetaData.ShouldPassMouse) { - mouseMapDrawAction = renderTargetSize => { - handle.UseShader(_blockColorInstance); - handle.SetTransform(CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition - (frame.Size / 2))); - handle.DrawTextureRect(pong.Texture, new Box2(Vector2.Zero, pong.Size), targetColor); - handle.UseShader(null); - handle.SetTransform(Matrix3.Identity); - }; - } else { - mouseMapDrawAction = null; - } - - ReturnRenderTarget(ping); - _renderTargetsToReturn.Push(pong); - return (iconDrawAction, mouseMapDrawAction); + DrawIconFast(handle, renderTargetSize, frame, pixelPosition, GetBlendAndColorShader(iconMetaData)); + } else { + //Slower path for filtered icons + DrawIconSlow(handle, frame, iconMetaData, renderTargetSize, pixelPosition); } } @@ -611,7 +472,7 @@ private void ClearPlanes() { var plane = pair.Value; // We can remove the plane if there was nothing on it last frame - if (plane.IconDrawActions.Count == 0 && plane.MouseMapDrawActions.Count == 0 && plane.Master == null) { + if (plane.Sprites.Count == 0 && plane.Master == null) { plane.Dispose(); _planes.Remove(pair.Key); continue; @@ -642,10 +503,10 @@ private void ProcessSprites(DrawingHandleWorld handle, Vector2i viewportSize, Bo if (!string.IsNullOrEmpty(sprite.RenderTarget)) { //if this sprite has a render target, draw it to a slate instead. If it needs to be drawn on the map, a second sprite instance will already have been created for that purpose - if (!_renderSourceLookup.TryGetValue(sprite.RenderTarget, out var tmpRenderTarget)) { + if (!RenderSourceLookup.TryGetValue(sprite.RenderTarget, out var tmpRenderTarget)) { tmpRenderTarget = RentRenderTarget(viewportSize); ClearRenderTarget(tmpRenderTarget, handle, new Color()); - _renderSourceLookup.Add(sprite.RenderTarget, tmpRenderTarget); + RenderSourceLookup.Add(sprite.RenderTarget, tmpRenderTarget); _renderTargetsToReturn.Push(tmpRenderTarget); } @@ -655,7 +516,7 @@ private void ProcessSprites(DrawingHandleWorld handle, Vector2i viewportSize, Bo plane.SetTemporaryRenderTarget(tmpRenderTarget); } else { //if not a plane master, draw the sprite to the render target //note we don't draw this to the mouse-map because that's handled when the RenderTarget is used as a source later - DrawIconNow(handle, tmpRenderTarget.Size, tmpRenderTarget, sprite, ((worldAABB.Size/2)-sprite.Position)-new Vector2(0.5f,0.5f), null, true); //draw the sprite centered on the RenderTarget + DrawOnRenderTarget(handle, tmpRenderTarget, sprite, worldAABB); } } else { //We are no longer dealing with RenderTargets, just regular old planes, so we collect the draw actions for batching //if this is a plane master then we don't render it, we just set it as the plane's master @@ -667,35 +528,37 @@ private void ProcessSprites(DrawingHandleWorld handle, Vector2i viewportSize, Bo } //add this sprite for rendering - (Action?, Action?) drawActions; - if (sprite.HasRenderSource && _renderSourceLookup.TryGetValue(sprite.RenderSource, out var renderSourceTexture)) { - drawActions = DrawIconAction(handle, sprite, (-worldAABB.BottomLeft)-(worldAABB.Size/2)+new Vector2(0.5f,0.5f), renderSourceTexture.Texture); - } else { - drawActions = DrawIconAction(handle, sprite, -worldAABB.BottomLeft); - } - - if (drawActions.Item1 != null) - plane.IconDrawActions.Add(drawActions.Item1); - if (drawActions.Item2 != null) - plane.MouseMapDrawActions.Add(drawActions.Item2); + plane.Sprites.Add(sprite); } } } - private void DrawPlanes(DrawingHandleWorld handle) { + /// + /// Used by to render an icon onto its render_target. + /// In a separate method to prevent unused closure allocations. + /// + private void DrawOnRenderTarget(DrawingHandleWorld handle, IRenderTarget renderTarget, RendererMetaData sprite, Box2 worldAABB) { + handle.RenderInRenderTarget(renderTarget, () => { + //draw the sprite centered on the RenderTarget + DrawIcon(handle, renderTarget.Size, sprite, ((worldAABB.Size/2)-sprite.Position)-new Vector2(0.5f,0.5f)); + }, null); + } + + private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { using (var _ = _prof.Group("draw planes map")) { handle.RenderInRenderTarget(_baseRenderTarget!, () => { foreach (int planeIndex in _planes.Keys.Order()) { var plane = _planes[planeIndex]; - plane.Draw(this, handle); + plane.Draw(this, handle, worldAABB); if (plane.Master != null) { // Don't draw this to the base render target if this itself is a render target if (!string.IsNullOrEmpty(plane.Master.RenderTarget)) continue; - DrawIconNow(handle, _baseRenderTarget!.Size, null, plane.Master, Vector2.Zero, plane.RenderTarget.Texture, noMouseMap: true); + plane.Master.TextureOverride = plane.RenderTarget.Texture; + DrawIcon(handle, _baseRenderTarget!.Size, plane.Master, Vector2.Zero); } else { handle.SetTransform(CreateRenderTargetFlipMatrix(_baseRenderTarget!.Size, Vector2.Zero)); handle.DrawTextureRect(plane.RenderTarget.Texture, Box2.FromTwoPoints(Vector2.Zero, _baseRenderTarget.Size)); @@ -704,15 +567,16 @@ private void DrawPlanes(DrawingHandleWorld handle) { }, null); } + // TODO: Can this only be done once the user clicks? using (_prof.Group("draw planes mouse map")) { handle.RenderInRenderTarget(_mouseMapRenderTarget!, () => { foreach (int planeIndex in _planes.Keys.Order()) - _planes[planeIndex].DrawMouseMap(_mouseMapRenderTarget!.Size); + _planes[planeIndex].DrawMouseMap(handle, this, _mouseMapRenderTarget!.Size, worldAABB); }, null); } } - private ViewAlgorithm.Tile?[,] CalculateTileVisibility(MapGridComponent grid, HashSet entities, TileRef eyeTile, int seeVis) { + private ViewAlgorithm.Tile?[,] CalculateTileVisibility(EntityUid gridUid, MapGridComponent grid, HashSet entities, TileRef eyeTile, int seeVis) { using var _ = _prof.Group("visible turfs"); var viewRange = _interfaceManager.View; @@ -722,11 +586,12 @@ private void DrawPlanes(DrawingHandleWorld handle) { _tileInfo = new ViewAlgorithm.Tile[viewRange.Width + 2, viewRange.Height + 2]; } - var eyeWorldPos = grid.GridTileToWorld(eyeTile.GridIndices); - var tileRefs = grid.GetTilesIntersecting(Box2.CenteredAround(eyeWorldPos.Position, new Vector2(_tileInfo.GetLength(0), _tileInfo.GetLength(1)))); + var eyeWorldPos = _mapSystem.GridTileToWorld(gridUid, grid, eyeTile.GridIndices); + var tileRefs = _mapSystem.GetTilesEnumerator(gridUid, grid, + Box2.CenteredAround(eyeWorldPos.Position, new Vector2(_tileInfo.GetLength(0), _tileInfo.GetLength(1)))); // Gather up all the data the view algorithm needs - foreach (TileRef tileRef in tileRefs) { + while (tileRefs.MoveNext(out var tileRef)) { var delta = tileRef.GridIndices - eyeTile.GridIndices; var appearance = _appearanceSystem.GetTurfIcon(tileRef.Tile.TypeId).Appearance; if (appearance == null) @@ -760,7 +625,7 @@ private void DrawPlanes(DrawingHandleWorld handle) { continue; var worldPos = _transformSystem.GetWorldPosition(transform); - var tilePos = grid.WorldToTile(worldPos) - eyeTile.GridIndices + viewRange.Center; + var tilePos = _mapSystem.WorldToTile(gridUid, grid, worldPos) - eyeTile.GridIndices + viewRange.Center; if (tilePos.X < 0 || tilePos.Y < 0 || tilePos.X >= _tileInfo.GetLength(0) || tilePos.Y >= _tileInfo.GetLength(1)) continue; @@ -773,7 +638,7 @@ private void DrawPlanes(DrawingHandleWorld handle) { return _tileInfo; } - private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, MapGridComponent grid, TileRef eyeTile, HashSet entities, int seeVis, SightFlags sight, Box2 worldAABB) { + private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, HashSet entities, int seeVis, SightFlags sight, Box2 worldAABB) { _spriteContainer.Clear(); // This exists purely because the tiebreaker var needs to exist somewhere @@ -788,8 +653,8 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, MapGridComponen continue; Vector2i tilePos = eyeTile.GridIndices + (tile.DeltaX, tile.DeltaY); - TileRef tileRef = grid.GetTileRef(tilePos); - MapCoordinates worldPos = grid.GridTileToWorld(tilePos); + TileRef tileRef = _mapSystem.GetTileRef(gridUid, grid, tilePos); + MapCoordinates worldPos = _mapSystem.GridTileToWorld(gridUid, grid, tilePos); tValue = 0; //pass the turf coords for client.images lookup @@ -813,7 +678,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, MapGridComponen // Check for visibility if the eye doesn't have SEE_OBJS or SEE_MOBS // TODO: Differentiate between objs and mobs if ((sight & (SightFlags.SeeObjs|SightFlags.SeeMobs)) == 0 && _tileInfo != null) { - var tilePos = grid.WorldToTile(worldPos) - eyeTile.GridIndices + _interfaceManager.View.Center; + var tilePos = _mapSystem.WorldToTile(gridUid, grid, worldPos) - eyeTile.GridIndices + _interfaceManager.View.Center; if (tilePos.X < 0 || tilePos.Y < 0 || tilePos.X >= _tileInfo.GetLength(0) || tilePos.Y >= _tileInfo.GetLength(1)) continue; @@ -855,23 +720,6 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, MapGridComponen } } - private void DrawIconNow(DrawingHandleWorld handle, Vector2i renderTargetSize, IRenderTarget? renderTarget, RendererMetaData iconMetaData, Vector2 positionOffset, Texture? textureOverride = null, bool noMouseMap = false) { - (Action? iconDrawAction, Action? mouseMapDrawAction) = DrawIconAction(handle, iconMetaData, positionOffset, textureOverride); - - if (iconDrawAction == null) - return; - - if (renderTarget != null) { - handle.RenderInRenderTarget(renderTarget, () => iconDrawAction(renderTargetSize), null); - } else { - iconDrawAction(renderTargetSize); - } - - if (mouseMapDrawAction != null && !noMouseMap) { - handle.RenderInRenderTarget(_mouseMapRenderTarget!, () => mouseMapDrawAction(renderTargetSize), null); - } - } - private RendererMetaData RentRendererMetaData() { RendererMetaData result; if (_rendererMetaDataRental.Count == 0) @@ -885,6 +733,119 @@ private RendererMetaData RentRendererMetaData() { return result; } + /// + /// Collect all of an icon's keep-together group and render them into one texture. + /// + private Texture ProcessKeepTogether(DrawingHandleWorld handle, RendererMetaData iconMetaData, Vector2i size) { + //store the parent's transform, color, blend, and alpha - then clear them for drawing to the render target + Matrix3 ktParentTransform = iconMetaData.TransformToApply; + Color ktParentColor = iconMetaData.ColorToApply; + float ktParentAlpha = iconMetaData.AlphaToApply; + BlendMode ktParentBlendMode = iconMetaData.BlendMode; + + iconMetaData.TransformToApply = Matrix3.Identity; + iconMetaData.ColorToApply = Color.White; + iconMetaData.AlphaToApply = 1f; + iconMetaData.BlendMode = BlendMode.Default; + + List ktItems = new List(iconMetaData.KeepTogetherGroup!.Count+1); + ktItems.Add(iconMetaData); + ktItems.AddRange(iconMetaData.KeepTogetherGroup); + iconMetaData.KeepTogetherGroup.Clear(); + + ktItems.Sort(); + //draw it onto an additional render target that we can return immediately for correction of transform + IRenderTexture tempTexture = RentRenderTarget(size); + ClearRenderTarget(tempTexture, handle, Color.Transparent); + + handle.RenderInRenderTarget(tempTexture, () => { + foreach (RendererMetaData ktItem in ktItems) { + DrawIcon(handle, tempTexture.Size, ktItem, -ktItem.Position); + } + }, null); + + //but keep the handle to the final KT group's render target so we don't override it later in the render cycle + IRenderTexture ktTexture = RentRenderTarget(tempTexture.Size); + handle.RenderInRenderTarget(ktTexture, () => { + handle.SetTransform(CreateRenderTargetFlipMatrix(tempTexture.Size, Vector2.Zero)); + handle.DrawTextureRect(tempTexture.Texture, new Box2(Vector2.Zero, tempTexture.Size)); + }, Color.Transparent); + + _renderTargetsToReturn.Push(tempTexture); + + //now restore the original color, alpha, blend, and transform so they can be applied to the render target as a whole + iconMetaData.TransformToApply = ktParentTransform; + iconMetaData.ColorToApply = ktParentColor; + iconMetaData.AlphaToApply = ktParentAlpha; + iconMetaData.BlendMode = ktParentBlendMode; + + _renderTargetsToReturn.Push(ktTexture); + return ktTexture.Texture; + } + + /// + /// Render a texture without applying any filters, making this faster and cheaper. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DrawIconFast(DrawingHandleWorld handle, Vector2i renderTargetSize, Texture texture, Vector2 pos, ShaderInstance? shader) { + handle.UseShader(shader); + handle.SetTransform(CreateRenderTargetFlipMatrix(renderTargetSize, pos)); + handle.DrawTextureRect(texture, Box2.FromDimensions(Vector2.Zero, texture.Size)); + } + + /// + /// A slower method of drawing an icon. This one renders an atom's filters. + /// Use instead if the icon has no special rendering needs. + /// + private void DrawIconSlow(DrawingHandleWorld handle, Texture frame, RendererMetaData iconMetaData, Vector2i renderTargetSize, Vector2 pos) { + //first we do ping pong rendering for the multiple filters + // TODO: This should determine the size from the filters and their settings, not just double the original + IRenderTexture ping = RentRenderTarget(frame.Size * 2); + IRenderTexture pong = RentRenderTarget(ping.Size); + + handle.RenderInRenderTarget(pong, () => { + //we can use the color matrix shader here, since we don't need to blend + //also because blend mode is none, we don't need to clear + var colorMatrix = iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity) + ? new ColorMatrix(iconMetaData.ColorToApply.WithAlpha(iconMetaData.AlphaToApply)) + : iconMetaData.ColorMatrixToApply; + + ShaderInstance colorShader = _colorInstance.Duplicate(); + colorShader.SetParameter("colorMatrix", colorMatrix.GetMatrix4()); + colorShader.SetParameter("offsetVector", colorMatrix.GetOffsetVector()); + colorShader.SetParameter("isPlaneMaster",iconMetaData.IsPlaneMaster); + handle.UseShader(colorShader); + + handle.SetTransform(CreateRenderTargetFlipMatrix(pong.Size, frame.Size / 2)); + handle.DrawTextureRect(frame, new Box2(Vector2.Zero, frame.Size)); + }, Color.Black.WithAlpha(0)); + + foreach (DreamFilter filterId in iconMetaData.MainIcon!.Appearance!.Filters) { + ShaderInstance s = _appearanceSystem.GetFilterShader(filterId, RenderSourceLookup); + + handle.RenderInRenderTarget(ping, () => { + handle.UseShader(s); + + // Technically this should be ping.Size, but they are the same size so avoid the extra closure alloc + var transform = CreateRenderTargetFlipMatrix(pong.Size, Vector2.Zero); + + handle.SetTransform(transform); + handle.DrawTextureRect(pong.Texture, new Box2(Vector2.Zero, pong.Size)); + }, Color.Black.WithAlpha(0)); + + (ping, pong) = (pong, ping); + } + + //then we draw the actual icon with filters applied + DrawIconFast(handle, renderTargetSize, pong.Texture, pos - frame.Size / 2, + //note we apply the color *before* the filters, so we ignore color here + GetBlendAndColorShader(iconMetaData, ignoreColor: true) + ); + + ReturnRenderTarget(ping); + _renderTargetsToReturn.Push(pong); + } + /// /// Creates a transformation matrix that counteracts RT's /// quirks @@ -894,10 +855,10 @@ private RendererMetaData RentRendererMetaData() { /// Size of the render target /// The translation to draw the icon at /// Due to RT applying transformations out of order, render the icon at Vector2.Zero - public Matrix3 CreateRenderTargetFlipMatrix(Vector2i renderTargetSize, Vector2 renderPosition) { + public static Matrix3 CreateRenderTargetFlipMatrix(Vector2i renderTargetSize, Vector2 renderPosition) { // RT flips the texture when doing a RenderInRenderTarget(), so we use _flipMatrix to reverse it // We must also handle translations here, since RT applies its own transform in an unexpected order - return _flipMatrix * Matrix3.CreateTranslation(renderPosition.X, renderTargetSize.Y - renderPosition.Y); + return FlipMatrix * Matrix3.CreateTranslation(renderPosition.X, renderTargetSize.Y - renderPosition.Y); } } @@ -920,7 +881,9 @@ internal sealed class RendererMetaData : IComparable { public AppearanceFlags AppearanceFlags; public BlendMode BlendMode; public MouseOpacity MouseOpacity; + public Texture? TextureOverride; + public Texture? Texture => TextureOverride ?? MainIcon?.CurrentFrame; public bool IsPlaneMaster => (AppearanceFlags & AppearanceFlags.PlaneMaster) != 0; public bool HasRenderSource => !string.IsNullOrEmpty(RenderSource); public bool ShouldPassMouse => HasRenderSource && (AppearanceFlags & AppearanceFlags.PassMouse) != 0; @@ -948,6 +911,7 @@ public void Reset() { AppearanceFlags = AppearanceFlags.None; BlendMode = BlendMode.Default; MouseOpacity = MouseOpacity.Transparent; + TextureOverride = null; } public int CompareTo(RendererMetaData? other) { @@ -1026,6 +990,7 @@ public int CompareTo(RendererMetaData? other) { #region Render Toggle Commands public sealed class ToggleScreenOverlayCommand : IConsoleCommand { + // ReSharper disable once StringLiteralTypo public string Command => "togglescreenoverlay"; public string Description => "Toggle rendering of screen objects"; public string Help => ""; @@ -1045,6 +1010,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) { } public sealed class ToggleMouseOverlayCommand : IConsoleCommand { + // ReSharper disable once StringLiteralTypo public string Command => "togglemouseoverlay"; public string Description => "Toggle rendering of mouse click area for screen objects"; public string Help => ""; From a8de7505cb0f2abe91d16db22dc6cfcc3a545168 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 17 Jan 2024 16:08:13 -0500 Subject: [PATCH 52/64] Update RT to v206.0.0 (#1624) --- .../Content.IntegrationTests.csproj | 6 +- Content.IntegrationTests/GameTests.cs | 2 +- Content.IntegrationTests/SetupCompileDM.cs | 2 +- Content.Tests/Content.Tests.csproj | 8 +- Content.Tests/ContentUnitTest.cs | 76 ++-- Content.Tests/DMTests.cs | 369 +++++++++--------- DMCompiler/DMCompiler.csproj | 3 - DMDisassembler/DMDisassembler.csproj | 2 - Directory.Packages.props | 7 + OpenDream.sln | 13 - OpenDreamClient/OpenDreamClient.csproj | 3 - .../OpenDreamPackageTool.csproj | 2 - OpenDreamPackaging/OpenDreamPackaging.csproj | 2 - OpenDreamRuntime/Objects/DreamIcon.cs | 21 +- OpenDreamRuntime/OpenDreamRuntime.csproj | 2 +- OpenDreamShared/OpenDreamShared.csproj | 2 +- RobustToolbox | 2 +- 17 files changed, 242 insertions(+), 280 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Content.IntegrationTests/Content.IntegrationTests.csproj b/Content.IntegrationTests/Content.IntegrationTests.csproj index 71b32f8827..9d4d0aa0dd 100644 --- a/Content.IntegrationTests/Content.IntegrationTests.csproj +++ b/Content.IntegrationTests/Content.IntegrationTests.csproj @@ -8,9 +8,9 @@ 12 - - - + + + diff --git a/Content.IntegrationTests/GameTests.cs b/Content.IntegrationTests/GameTests.cs index 2f2319afba..8ccd74f02c 100644 --- a/Content.IntegrationTests/GameTests.cs +++ b/Content.IntegrationTests/GameTests.cs @@ -13,7 +13,7 @@ public sealed class GameTests : ContentIntegrationTest { public async Task NoRuntimesTest() { var (client, server) = await StartConnectedServerClientPair(); await RunTicksSync(client, server, 1000); - Assert.IsTrue(server.IsAlive); + Assert.That(server.IsAlive); var manager = server.ResolveDependency(); if(manager.LastDMException is not null) { Assert.Fail($"Runtime occurred on server boot: {manager.LastDMException}"); diff --git a/Content.IntegrationTests/SetupCompileDM.cs b/Content.IntegrationTests/SetupCompileDM.cs index f9ec1f9159..ff532c0ddb 100644 --- a/Content.IntegrationTests/SetupCompileDM.cs +++ b/Content.IntegrationTests/SetupCompileDM.cs @@ -19,7 +19,7 @@ public void Compile() { Files = new() { DmEnvironment } }); - Assert.IsTrue(successfulCompile && File.Exists(CompiledProject), "Failed to compile DM test project!"); + Assert.That(successfulCompile && File.Exists(CompiledProject), "Failed to compile DM test project!"); } [OneTimeTearDown] diff --git a/Content.Tests/Content.Tests.csproj b/Content.Tests/Content.Tests.csproj index 719446e02d..44a4f44227 100644 --- a/Content.Tests/Content.Tests.csproj +++ b/Content.Tests/Content.Tests.csproj @@ -11,10 +11,10 @@ enable - - - - + + + + diff --git a/Content.Tests/ContentUnitTest.cs b/Content.Tests/ContentUnitTest.cs index 477e8da908..c102584fb6 100644 --- a/Content.Tests/ContentUnitTest.cs +++ b/Content.Tests/ContentUnitTest.cs @@ -1,55 +1,51 @@ +using System; using System.Collections.Generic; using System.Reflection; using OpenDreamClient; using OpenDreamRuntime; +using OpenDreamRuntime.Rendering; using OpenDreamShared; +using OpenDreamShared.Rendering; using Robust.Shared.Analyzers; using Robust.Shared.IoC; using Robust.UnitTesting; using EntryPoint = OpenDreamRuntime.EntryPoint; -namespace Content.Tests -{ - [Virtual] - public class ContentUnitTest : RobustUnitTest - { - protected override void OverrideIoC() - { - base.OverrideIoC(); - - SharedOpenDreamIoC.Register(); - - if (Project == UnitTestProject.Server) - { - ServerContentIoC.Register(unitTests: true); - IoCManager.Register(); - } - else if (Project == UnitTestProject.Client) - { - ClientContentIoC.Register(); - } +namespace Content.Tests; + +[Virtual] +public class ContentUnitTest : RobustUnitTest { + protected override Type[] ExtraComponents { get; } = { + typeof(DMISpriteComponent), + typeof(DreamMobSightComponent) + }; + + protected override void OverrideIoC() { + base.OverrideIoC(); + + SharedOpenDreamIoC.Register(); + + if (Project == UnitTestProject.Server) { + ServerContentIoC.Register(unitTests: true); + IoCManager.Register(); + } else if (Project == UnitTestProject.Client) { + ClientContentIoC.Register(); } + } - protected override Assembly[] GetContentAssemblies() - { - var l = new List - { - typeof(OpenDreamShared.EntryPoint).Assembly - }; - - if (Project == UnitTestProject.Server) - { - l.Add(typeof(EntryPoint).Assembly); - } - else if (Project == UnitTestProject.Client) - { - l.Add(typeof(OpenDreamClient.EntryPoint).Assembly); - } - - l.Add(typeof(ContentUnitTest).Assembly); - - return l.ToArray(); + protected override Assembly[] GetContentAssemblies() { + var l = new List { + typeof(OpenDreamShared.EntryPoint).Assembly + }; + + if (Project == UnitTestProject.Server) { + l.Add(typeof(EntryPoint).Assembly); + } else if (Project == UnitTestProject.Client) { + l.Add(typeof(OpenDreamClient.EntryPoint).Assembly); } + + l.Add(typeof(ContentUnitTest).Assembly); + + return l.ToArray(); } } - diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 59b69dde28..4fec7eba4c 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -5,235 +5,220 @@ using NUnit.Framework; using OpenDreamRuntime; using OpenDreamRuntime.Objects; -using OpenDreamRuntime.Rendering; -using OpenDreamShared.Rendering; using Robust.Shared.Asynchronous; -using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Timing; -// Disables warnings about using the constraint model; May need to be updated. -#pragma warning disable NUnit2002 // We use Assert.IsFalse -#pragma warning disable NUnit2003 // We use Assert.IsTrue -#pragma warning disable NUnit2010 // We do not use the constraint model -#pragma warning disable NUnit2017 // We use Assert.IsNull - -namespace Content.Tests -{ - [TestFixture] - public sealed class DMTests : ContentUnitTest { - private const string TestProject = "DMProject"; - private const string InitializeEnvironment = "./environment.dme"; - private const string TestsDirectory = "Tests"; - - [Dependency] private readonly DreamManager _dreamMan = default!; - [Dependency] private readonly DreamObjectTree _objectTree = default!; - [Dependency] private readonly ITaskManager _taskManager = default!; - - [Flags] - public enum DMTestFlags { - NoError = 0, // Should run without errors - Ignore = 1, // Ignore entirely - CompileError = 2, // Should fail to compile - RuntimeError = 4, // Should throw an exception at runtime - ReturnTrue = 8, // Should return TRUE - NoReturn = 16, // Shouldn't return (aka stopped by a stack-overflow or runtimes) - } +namespace Content.Tests; + +[TestFixture] +public sealed class DMTests : ContentUnitTest { + private const string TestProject = "DMProject"; + private const string InitializeEnvironment = "./environment.dme"; + private const string TestsDirectory = "Tests"; + + [Dependency] private readonly DreamManager _dreamMan = default!; + [Dependency] private readonly DreamObjectTree _objectTree = default!; + [Dependency] private readonly ITaskManager _taskManager = default!; + + [Flags] + public enum DMTestFlags { + NoError = 0, // Should run without errors + Ignore = 1, // Ignore entirely + CompileError = 2, // Should fail to compile + RuntimeError = 4, // Should throw an exception at runtime + ReturnTrue = 8, // Should return TRUE + NoReturn = 16, // Shouldn't return (aka stopped by a stack-overflow or runtimes) + } - private void OnException(object? sender, Exception exception) => TestContext.WriteLine(exception); - - [OneTimeSetUp] - public void OneTimeSetup() { - IoCManager.InjectDependencies(this); - _taskManager.Initialize(); - IComponentFactory componentFactory = IoCManager.Resolve(); - componentFactory.RegisterClass(); - componentFactory.RegisterClass(); //wow this is terrible TODO figure out why this is necessary - componentFactory.GenerateNetIds(); - Compile(InitializeEnvironment); - _dreamMan.PreInitialize(Path.ChangeExtension(InitializeEnvironment, "json")); - _dreamMan.OnException += OnException; - } + private void OnException(object? sender, Exception exception) => TestContext.WriteLine(exception); - private static string? Compile(string sourceFile) { - bool successfulCompile = DMCompiler.DMCompiler.Compile(new() { - Files = new() { sourceFile } - }); + [OneTimeSetUp] + public void OneTimeSetup() { + IoCManager.InjectDependencies(this); + _taskManager.Initialize(); + Compile(InitializeEnvironment); + _dreamMan.PreInitialize(Path.ChangeExtension(InitializeEnvironment, "json")); + _dreamMan.OnException += OnException; + } - return successfulCompile ? Path.ChangeExtension(sourceFile, "json") : null; - } + private static string? Compile(string sourceFile) { + bool successfulCompile = DMCompiler.DMCompiler.Compile(new() { + Files = new() { sourceFile } + }); - private static void Cleanup(string? compiledFile) { - if (!File.Exists(compiledFile)) - return; + return successfulCompile ? Path.ChangeExtension(sourceFile, "json") : null; + } - File.Delete(compiledFile); - } + private static void Cleanup(string? compiledFile) { + if (!File.Exists(compiledFile)) + return; - [Test, TestCaseSource(nameof(GetTests))] - public void TestFiles(string sourceFile, DMTestFlags testFlags) { - string initialDirectory = Directory.GetCurrentDirectory(); - TestContext.WriteLine($"--- TEST {sourceFile} | Flags: {testFlags}"); - try { - string? compiledFile = Compile(Path.Join(initialDirectory, TestsDirectory, sourceFile)); - if (testFlags.HasFlag(DMTestFlags.CompileError)) { - Assert.IsNull(compiledFile, "Expected an error during DM compilation"); - Cleanup(compiledFile); - TestContext.WriteLine($"--- PASS {sourceFile}"); - return; - } - - Assert.IsTrue(compiledFile is not null && File.Exists(compiledFile), "Failed to compile DM source file"); - Assert.IsTrue(_dreamMan.LoadJson(compiledFile), $"Failed to load {compiledFile}"); - _dreamMan.StartWorld(); - - (bool successfulRun, DreamValue? returned, Exception? exception) = RunTest(); - - if (testFlags.HasFlag(DMTestFlags.NoReturn)) { - Assert.IsFalse(returned.HasValue, "proc returned unexpectedly"); - } else { - Assert.IsTrue(returned.HasValue, "proc did not return (did it hit an exception?)"); - } - - if (testFlags.HasFlag(DMTestFlags.RuntimeError)) { - Assert.IsFalse(successfulRun, "A DM runtime exception was expected"); - } else { - if (exception != null) - Assert.IsTrue(successfulRun, $"A DM runtime exception was thrown: \"{exception}\""); - else - Assert.IsTrue(successfulRun, "A DM runtime exception was thrown, and its message could not be recovered!"); - } - - if (testFlags.HasFlag(DMTestFlags.ReturnTrue)) { - Assert.That(returned.HasValue, Is.True); - Assert.IsTrue(returned.Value.IsTruthy(), "Test was expected to return TRUE"); - } + File.Delete(compiledFile); + } + [Test, TestCaseSource(nameof(GetTests))] + public void TestFiles(string sourceFile, DMTestFlags testFlags) { + string initialDirectory = Directory.GetCurrentDirectory(); + TestContext.WriteLine($"--- TEST {sourceFile} | Flags: {testFlags}"); + try { + string? compiledFile = Compile(Path.Join(initialDirectory, TestsDirectory, sourceFile)); + if (testFlags.HasFlag(DMTestFlags.CompileError)) { + Assert.That(compiledFile, Is.Null, "Expected an error during DM compilation"); Cleanup(compiledFile); TestContext.WriteLine($"--- PASS {sourceFile}"); - } finally { - // Restore the original CurrentDirectory, since loading a compiled JSON changes it. - Directory.SetCurrentDirectory(initialDirectory); + return; } - } - private (bool Success, DreamValue? Returned, Exception? except) RunTest() { - var prev = _dreamMan.LastDMException; - - DreamValue? retValue = null; - Task callTask = null!; - - DreamThread.Run("RunTest", async (state) => { - if (_objectTree.TryGetGlobalProc("RunTest", out DreamProc? proc)) { - callTask = state.Call(proc, null, null); - retValue = await callTask; - return DreamValue.Null; - } else { - Assert.Fail("No global proc named RunTest"); - return DreamValue.Null; - } - }); - - var watch = new Stopwatch(); - watch.Start(); - - // Tick until our inner call has finished - while (!callTask.IsCompleted) { - _dreamMan.Update(); - _taskManager.ProcessPendingTasks(); - - if (watch.Elapsed.TotalMilliseconds > 500) { - Assert.Fail("Test timed out"); - } + Assert.That(compiledFile is not null && File.Exists(compiledFile), "Failed to compile DM source file"); + Assert.That(_dreamMan.LoadJson(compiledFile), $"Failed to load {compiledFile}"); + _dreamMan.StartWorld(); + + (bool successfulRun, DreamValue? returned, Exception? exception) = RunTest(); + + if (testFlags.HasFlag(DMTestFlags.NoReturn)) { + Assert.That(returned.HasValue, Is.False, "proc returned unexpectedly"); + } else { + Assert.That(returned.HasValue, "proc did not return (did it hit an exception?)"); } - bool retSuccess = _dreamMan.LastDMException == prev; // Works because "null == null" is true in this language. - return (retSuccess, retValue, _dreamMan.LastDMException); + if (testFlags.HasFlag(DMTestFlags.RuntimeError)) { + Assert.That(successfulRun, Is.False, "A DM runtime exception was expected"); + } else { + if (exception != null) + Assert.That(successfulRun, $"A DM runtime exception was thrown: \"{exception}\""); + else + Assert.That(successfulRun, "A DM runtime exception was thrown, and its message could not be recovered!"); + } + + if (testFlags.HasFlag(DMTestFlags.ReturnTrue)) { + Assert.That(returned?.IsTruthy(), Is.True, "Test was expected to return TRUE"); + } + + Cleanup(compiledFile); + TestContext.WriteLine($"--- PASS {sourceFile}"); + } finally { + // Restore the original CurrentDirectory, since loading a compiled JSON changes it. + Directory.SetCurrentDirectory(initialDirectory); } + } - private static IEnumerable GetTests() - { - Directory.SetCurrentDirectory(TestProject); + private (bool Success, DreamValue? Returned, Exception? except) RunTest() { + var prev = _dreamMan.LastDMException; - foreach (string sourceFile in Directory.GetFiles(TestsDirectory, "*.dm", SearchOption.AllDirectories)) { - string sourceFile2 = sourceFile[$"{TestsDirectory}/".Length..]; - DMTestFlags testFlags = GetDMTestFlags(sourceFile); - if (testFlags.HasFlag(DMTestFlags.Ignore)) - continue; + DreamValue? retValue = null; + Task callTask = null!; - yield return new object[] { - sourceFile2, - testFlags - }; + DreamThread.Run("RunTest", async (state) => { + if (_objectTree.TryGetGlobalProc("RunTest", out DreamProc? proc)) { + callTask = state.Call(proc, null, null); + retValue = await callTask; + return DreamValue.Null; + } else { + Assert.Fail("No global proc named RunTest"); + return DreamValue.Null; } - } + }); + + var watch = new Stopwatch(); + watch.Start(); + + // Tick until our inner call has finished + while (!callTask.IsCompleted) { + _dreamMan.Update(); + _taskManager.ProcessPendingTasks(); - private static DMTestFlags GetDMTestFlags(string sourceFile) { - DMTestFlags testFlags = DMTestFlags.NoError; - - using (StreamReader reader = new StreamReader(sourceFile)) { - string? firstLine = reader.ReadLine(); - if (firstLine == null) - return testFlags; - if (firstLine.Contains("IGNORE", StringComparison.InvariantCulture)) - testFlags |= DMTestFlags.Ignore; - if (firstLine.Contains("COMPILE ERROR", StringComparison.InvariantCulture)) - testFlags |= DMTestFlags.CompileError; - if (firstLine.Contains("RUNTIME ERROR", StringComparison.InvariantCulture)) - testFlags |= DMTestFlags.RuntimeError; - if (firstLine.Contains("RETURN TRUE", StringComparison.InvariantCulture)) - testFlags |= DMTestFlags.ReturnTrue; - if (firstLine.Contains("NO RETURN", StringComparison.InvariantCulture)) - testFlags |= DMTestFlags.NoReturn; + if (watch.Elapsed.TotalMilliseconds > 500) { + Assert.Fail("Test timed out"); } + } + + bool retSuccess = _dreamMan.LastDMException == prev; // Works because "null == null" is true in this language. + return (retSuccess, retValue, _dreamMan.LastDMException); + } + + private static IEnumerable GetTests() + { + Directory.SetCurrentDirectory(TestProject); - return testFlags; + foreach (string sourceFile in Directory.GetFiles(TestsDirectory, "*.dm", SearchOption.AllDirectories)) { + string sourceFile2 = sourceFile[$"{TestsDirectory}/".Length..]; + DMTestFlags testFlags = GetDMTestFlags(sourceFile); + if (testFlags.HasFlag(DMTestFlags.Ignore)) + continue; + + yield return new object[] { + sourceFile2, + testFlags + }; } + } - // TODO Convert the below async tests + private static DMTestFlags GetDMTestFlags(string sourceFile) { + DMTestFlags testFlags = DMTestFlags.NoError; + + using (StreamReader reader = new StreamReader(sourceFile)) { + string? firstLine = reader.ReadLine(); + if (firstLine == null) + return testFlags; + if (firstLine.Contains("IGNORE", StringComparison.InvariantCulture)) + testFlags |= DMTestFlags.Ignore; + if (firstLine.Contains("COMPILE ERROR", StringComparison.InvariantCulture)) + testFlags |= DMTestFlags.CompileError; + if (firstLine.Contains("RUNTIME ERROR", StringComparison.InvariantCulture)) + testFlags |= DMTestFlags.RuntimeError; + if (firstLine.Contains("RETURN TRUE", StringComparison.InvariantCulture)) + testFlags |= DMTestFlags.ReturnTrue; + if (firstLine.Contains("NO RETURN", StringComparison.InvariantCulture)) + testFlags |= DMTestFlags.NoReturn; + } - /*[Test, Timeout(10000)] - public void AsyncCall() { + return testFlags; + } - var result = DreamValue.Null; + // TODO Convert the below async tests - DreamThread.Run(async(state) => { - var world = _dreamMan.WorldInstance; - var proc = world.GetProc("async_test"); - result = await state.Call(proc, world, null, new DreamProcArguments(null)); - state.Runtime.Shutdown = true; - return DreamValue.Null; - }); + /*[Test, Timeout(10000)] + public void AsyncCall() { - runtime.Run(); + var result = DreamValue.Null; - Assert.AreEqual(new DreamValue(1337), result); - Assert.That(_dreamMan.DMExceptionCount, Is.EqualTo(prev)); - }*/ + DreamThread.Run(async(state) => { + var world = _dreamMan.WorldInstance; + var proc = world.GetProc("async_test"); + result = await state.Call(proc, world, null, new DreamProcArguments(null)); + state.Runtime.Shutdown = true; + return DreamValue.Null; + }); - /*[Test, Timeout(10000)] - public void WaitFor() { + runtime.Run(); - DreamValue result_1 = DreamValue.Null; - DreamValue result_2 = DreamValue.Null; + Assert.AreEqual(new DreamValue(1337), result); + Assert.That(_dreamMan.DMExceptionCount, Is.EqualTo(prev)); + }*/ - DreamThread.Run(async(state) => { - var world = _dreamMan.WorldInstance; - var proc_1 = world.GetProc("waitfor_1_a"); - result_1 = await state.Call(proc_1, world, null, new DreamProcArguments(null)); + /*[Test, Timeout(10000)] + public void WaitFor() { - var proc_2 = world.GetProc("waitfor_2_a"); - result_2 = await state.Call(proc_2, world, null, new DreamProcArguments(null)); + DreamValue result_1 = DreamValue.Null; + DreamValue result_2 = DreamValue.Null; - state.Runtime.Shutdown = true; - return DreamValue.Null; - }); + DreamThread.Run(async(state) => { + var world = _dreamMan.WorldInstance; + var proc_1 = world.GetProc("waitfor_1_a"); + result_1 = await state.Call(proc_1, world, null, new DreamProcArguments(null)); - runtime.Run(); + var proc_2 = world.GetProc("waitfor_2_a"); + result_2 = await state.Call(proc_2, world, null, new DreamProcArguments(null)); - Assert.AreEqual(new DreamValue(3), result_1); - Assert.AreEqual(new DreamValue(2), result_2); - Assert.That(_dreamMan.DMExceptionCount, Is.EqualTo(prev)); - }*/ - } + state.Runtime.Shutdown = true; + return DreamValue.Null; + }); + + runtime.Run(); + + Assert.AreEqual(new DreamValue(3), result_1); + Assert.AreEqual(new DreamValue(2), result_2); + Assert.That(_dreamMan.DMExceptionCount, Is.EqualTo(prev)); + }*/ } diff --git a/DMCompiler/DMCompiler.csproj b/DMCompiler/DMCompiler.csproj index 53b82d0b6c..82cddd4ad9 100644 --- a/DMCompiler/DMCompiler.csproj +++ b/DMCompiler/DMCompiler.csproj @@ -1,5 +1,4 @@  - Exe net8.0 @@ -24,6 +23,4 @@ - - diff --git a/DMDisassembler/DMDisassembler.csproj b/DMDisassembler/DMDisassembler.csproj index e116fcf4e6..6355f4bb4f 100644 --- a/DMDisassembler/DMDisassembler.csproj +++ b/DMDisassembler/DMDisassembler.csproj @@ -1,5 +1,4 @@  - Exe net8.0 @@ -11,5 +10,4 @@ - diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000000..9e2d5a099a --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/OpenDream.sln b/OpenDream.sln index 7c81824472..35847e91f3 100644 --- a/OpenDream.sln +++ b/OpenDream.sln @@ -56,8 +56,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenToolkit.GraphicsLibrary EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.LoaderApi", "RobustToolbox\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj", "{7AA19C30-0627-473E-B3D0-08E96FC2D77D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robust.Physics", "RobustToolbox\Robust.Physics\Robust.Physics.csproj", "{B9712645-6DAD-4D51-BF56-8FE1422EDB3E}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XamlX", "XamlX", "{2E99CFE9-C7C6-4275-8730-2672E28CCEA4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamlX", "RobustToolbox\XamlX\src\XamlX\XamlX.csproj", "{ECB7A503-FF99-4E73-927A-B434915A452A}" @@ -280,16 +278,6 @@ Global {7AA19C30-0627-473E-B3D0-08E96FC2D77D}.Release|x64.Build.0 = Release|Any CPU {7AA19C30-0627-473E-B3D0-08E96FC2D77D}.Tools|Any CPU.ActiveCfg = Tools|Any CPU {7AA19C30-0627-473E-B3D0-08E96FC2D77D}.Tools|Any CPU.Build.0 = Tools|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Debug|x64.ActiveCfg = Debug|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Debug|x64.Build.0 = Debug|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Release|Any CPU.Build.0 = Release|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Release|x64.ActiveCfg = Release|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Release|x64.Build.0 = Release|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Tools|Any CPU.ActiveCfg = Tools|Any CPU - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E}.Tools|Any CPU.Build.0 = Tools|Any CPU {ECB7A503-FF99-4E73-927A-B434915A452A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ECB7A503-FF99-4E73-927A-B434915A452A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECB7A503-FF99-4E73-927A-B434915A452A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -461,7 +449,6 @@ Global {4A4148E4-C82C-45F5-8278-371CA0567EFB} = {DBFD7471-84E2-4AAB-86E9-F8DFF917ED5B} {D5D921FD-A535-40CE-AED2-0526AC4015E4} = {DBFD7471-84E2-4AAB-86E9-F8DFF917ED5B} {7AA19C30-0627-473E-B3D0-08E96FC2D77D} = {DBFD7471-84E2-4AAB-86E9-F8DFF917ED5B} - {B9712645-6DAD-4D51-BF56-8FE1422EDB3E} = {DBFD7471-84E2-4AAB-86E9-F8DFF917ED5B} {2E99CFE9-C7C6-4275-8730-2672E28CCEA4} = {DBFD7471-84E2-4AAB-86E9-F8DFF917ED5B} {ECB7A503-FF99-4E73-927A-B434915A452A} = {2E99CFE9-C7C6-4275-8730-2672E28CCEA4} {C598983A-4FE6-45FF-A6C2-1563394285E1} = {2E99CFE9-C7C6-4275-8730-2672E28CCEA4} diff --git a/OpenDreamClient/OpenDreamClient.csproj b/OpenDreamClient/OpenDreamClient.csproj index e06241556c..2ba9bcc9e4 100644 --- a/OpenDreamClient/OpenDreamClient.csproj +++ b/OpenDreamClient/OpenDreamClient.csproj @@ -9,9 +9,6 @@ Exe enable - - - diff --git a/OpenDreamPackageTool/OpenDreamPackageTool.csproj b/OpenDreamPackageTool/OpenDreamPackageTool.csproj index ac86916b7a..42742821dd 100644 --- a/OpenDreamPackageTool/OpenDreamPackageTool.csproj +++ b/OpenDreamPackageTool/OpenDreamPackageTool.csproj @@ -1,5 +1,4 @@ - Exe net8.0 @@ -10,5 +9,4 @@ - diff --git a/OpenDreamPackaging/OpenDreamPackaging.csproj b/OpenDreamPackaging/OpenDreamPackaging.csproj index 34748e8ac1..468cf35904 100644 --- a/OpenDreamPackaging/OpenDreamPackaging.csproj +++ b/OpenDreamPackaging/OpenDreamPackaging.csproj @@ -1,5 +1,4 @@ - Exe net8.0 @@ -10,5 +9,4 @@ - diff --git a/OpenDreamRuntime/Objects/DreamIcon.cs b/OpenDreamRuntime/Objects/DreamIcon.cs index 4d8320433c..e491281f42 100644 --- a/OpenDreamRuntime/Objects/DreamIcon.cs +++ b/OpenDreamRuntime/Objects/DreamIcon.cs @@ -5,7 +5,7 @@ using OpenDreamShared.Dream; using OpenDreamShared.Resources; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Color = Robust.Shared.Maths.Color; @@ -116,22 +116,21 @@ public IconResource GenerateDMI() { } } - Image dmiImage = Image.LoadPixelData(pixels, span, frameHeight); + Image dmiImage = Image.LoadPixelData(pixels, span, frameHeight); ParsedDMIDescription newDescription = new() {Width = frameWidth, Height = frameHeight, States = dmiStates}; PixelArrayPool.Return(pixels, clearArray: true); - using (MemoryStream dmiImageStream = new MemoryStream()) { - var pngTextData = new PngTextData("Description", newDescription.ExportAsText(), null, null); - var pngMetadata = dmiImage.Metadata.GetPngMetadata(); - pngMetadata.TextData.Add(pngTextData); + using var dmiImageStream = new MemoryStream(); + var pngTextData = new PngTextData("Description", newDescription.ExportAsText(), null, null); + var pngMetadata = dmiImage.Metadata.GetPngMetadata(); + pngMetadata.TextData.Add(pngTextData); - dmiImage.SaveAsPng(dmiImageStream); + dmiImage.SaveAsPng(dmiImageStream); - IconResource newResource = _resourceManager.CreateIconResource(dmiImageStream.GetBuffer(), dmiImage, newDescription); - _cachedDMI = newResource; - return _cachedDMI; - } + IconResource newResource = _resourceManager.CreateIconResource(dmiImageStream.GetBuffer(), dmiImage, newDescription); + _cachedDMI = newResource; + return _cachedDMI; } public void ApplyOperation(IDreamIconOperation operation) { diff --git a/OpenDreamRuntime/OpenDreamRuntime.csproj b/OpenDreamRuntime/OpenDreamRuntime.csproj index 6c861cf1b5..e6858df7e9 100644 --- a/OpenDreamRuntime/OpenDreamRuntime.csproj +++ b/OpenDreamRuntime/OpenDreamRuntime.csproj @@ -13,7 +13,7 @@ - + diff --git a/OpenDreamShared/OpenDreamShared.csproj b/OpenDreamShared/OpenDreamShared.csproj index 59d0a5476e..cfcc9c450a 100644 --- a/OpenDreamShared/OpenDreamShared.csproj +++ b/OpenDreamShared/OpenDreamShared.csproj @@ -10,7 +10,7 @@ enable - + diff --git a/RobustToolbox b/RobustToolbox index 73357f022b..e357dada65 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 73357f022ba3a0b60587b602f68e160df6d79648 +Subproject commit e357dada656776118c7cd05a8effaaaca74e8f9c From 2c1c73cc3f8c74a6cb6284a24516affe232ec7e7 Mon Sep 17 00:00:00 2001 From: wixoa Date: Sat, 20 Jan 2024 22:02:44 -0500 Subject: [PATCH 53/64] Fix edge-case positional/named argument handling in `image()` (#1627) --- OpenDreamRuntime/DreamManager.cs | 6 ++++-- OpenDreamRuntime/Procs/DMProc.cs | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 7453ae3c70..2bd61973cc 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -41,13 +41,14 @@ public sealed partial class DreamManager { // Global state that may not really (really really) belong here public DreamValue[] Globals { get; set; } = Array.Empty(); - public List GlobalNames { get; private set; } = new List(); + public List GlobalNames { get; private set; } = new(); public Dictionary ReferenceIDs { get; } = new(); public Dictionary ReferenceIDsToDreamObject { get; } = new(); public HashSet Clients { get; set; } = new(); public HashSet Datums { get; set; } = new(); public Random Random { get; set; } = new(); public Dictionary> Tags { get; set; } = new(); + public DreamProc ImageConstructor, ImageFactoryProc; private int _dreamObjectRefIdCounter; private DreamCompiledJson _compiledJson; @@ -123,8 +124,9 @@ public bool LoadJson(string? jsonPath) { throw new FileNotFoundException("Interface DMF not found at "+Path.Join(rootPath,_compiledJson.Interface)); _objectTree.LoadJson(json); - DreamProcNative.SetupNativeProcs(_objectTree); + ImageConstructor = _objectTree.Image.ObjectDefinition.GetProc("New"); + _objectTree.TryGetGlobalProc("image", out ImageFactoryProc!); _dreamMapManager.Initialize(); WorldInstance = new DreamObjectWorld(_objectTree.World.ObjectDefinition); diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index a5d2a3c796..72cce21987 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -977,6 +977,9 @@ public DreamProcArguments CreateProcArguments(ReadOnlySpan values, D var argumentCount = argumentStackSize / 2; var arguments = new DreamValue[Math.Max(argumentCount, proc.ArgumentNames.Count)]; + var skippingArg = false; + var isImageConstructor = proc == Proc.DreamManager.ImageConstructor || + proc == Proc.DreamManager.ImageFactoryProc; Array.Fill(arguments, DreamValue.Null); for (int i = 0; i < argumentCount; i++) { @@ -984,7 +987,15 @@ public DreamProcArguments CreateProcArguments(ReadOnlySpan values, D var value = values[i*2+1]; if (key.IsNull) { - arguments[i] = value; + // image() or new /image() will skip the loc arg if the second arg is a string + // Really don't like this but it's BYOND behavior + // Note that the way we're doing it leads to different argument placement when there are no named args + // Hopefully nothing depends on that though + // TODO: We aim to do sanity improvements in the future, yea? Big one here + if (isImageConstructor && i == 1 && value.Type == DreamValue.DreamValueType.String) + skippingArg = true; + + arguments[skippingArg ? i + 1 : i] = value; } else { string argumentName = key.MustGetValueAsString(); int argumentIndex = proc.ArgumentNames.IndexOf(argumentName); @@ -1005,6 +1016,9 @@ public DreamProcArguments CreateProcArguments(ReadOnlySpan values, D var listValues = argList.GetValues(); var arguments = new DreamValue[Math.Max(listValues.Count, proc.ArgumentNames.Count)]; + var skippingArg = false; + var isImageConstructor = proc == Proc.DreamManager.ImageConstructor || + proc == Proc.DreamManager.ImageFactoryProc; Array.Fill(arguments, DreamValue.Null); for (int i = 0; i < listValues.Count; i++) { @@ -1020,8 +1034,15 @@ public DreamProcArguments CreateProcArguments(ReadOnlySpan values, D arguments[argumentIndex] = argList.GetValue(value); } else { //Ordered argument + // image() or new /image() will skip the loc arg if the second arg is a string + // Really don't like this but it's BYOND behavior + // Note that the way we're doing it leads to different argument placement when there are no named args + // Hopefully nothing depends on that though + if (isImageConstructor && i == 1 && value.Type == DreamValue.DreamValueType.String) + skippingArg = true; + // TODO: Verify ordered args precede all named args - arguments[i] = value; + arguments[skippingArg ? i + 1 : i] = value; } } From 1c11807c86d51690ffc40993d50d87506be6b465 Mon Sep 17 00:00:00 2001 From: distributivgesetz Date: Mon, 22 Jan 2024 21:05:50 +0100 Subject: [PATCH 54/64] Tweak `Nameof` slightly, add `GetNameof()` support to more expressions (#1618) --- .../Tests/Special Procs/nameof/nameof.dm | 8 ++++++ DMCompiler/DM/DMExpression.cs | 14 ++++++---- DMCompiler/DM/DMProc.cs | 23 ++++++++------- DMCompiler/DM/Expressions/Builtins.cs | 28 +++++++++---------- DMCompiler/DM/Expressions/Constant.cs | 4 +-- DMCompiler/DM/Expressions/Dereference.cs | 7 +++++ DMCompiler/DM/Expressions/LValue.cs | 21 ++++++++------ DMCompiler/DM/Visitors/DMExpressionBuilder.cs | 13 ++++++++- 8 files changed, 72 insertions(+), 46 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Special Procs/nameof/nameof.dm b/Content.Tests/DMProject/Tests/Special Procs/nameof/nameof.dm index d3b575d4eb..562dbd9d0d 100644 --- a/Content.Tests/DMProject/Tests/Special Procs/nameof/nameof.dm +++ b/Content.Tests/DMProject/Tests/Special Procs/nameof/nameof.dm @@ -2,7 +2,14 @@ var/global/foobar /proc/meep() +/datum/test/two + var/name = "some name" + +/datum/test + var/datum/test/two/two + /datum/test/proc/testarg(atom/movable/A, B) + ASSERT(nameof(two) == "two") ASSERT(nameof(A) == "A") ASSERT(nameof(B) == "B") @@ -13,4 +20,5 @@ var/global/foobar ASSERT(nameof(/datum/test) == "test") ASSERT(nameof(global.foobar) == "foobar") var/datum/test/T = new + ASSERT(nameof(T.two.name) == "name") T.testarg(new /datum) // Just for fun we won't pass the arg's declared type diff --git a/DMCompiler/DM/DMExpression.cs b/DMCompiler/DM/DMExpression.cs index 31055cb394..1a740669ba 100644 --- a/DMCompiler/DM/DMExpression.cs +++ b/DMCompiler/DM/DMExpression.cs @@ -1,9 +1,9 @@ -using System; +using DMCompiler.Bytecode; +using DMCompiler.Compiler.DM; using DMCompiler.DM.Visitors; using OpenDreamShared.Compiler; -using DMCompiler.Compiler.DM; +using System; using System.Diagnostics.CodeAnalysis; -using DMCompiler.Bytecode; namespace DMCompiler.DM { internal abstract class DMExpression { @@ -60,9 +60,11 @@ public virtual DMReference EmitReference(DMObject dmObject, DMProc proc, string throw new CompileErrorException(Location, $"attempt to reference r-value"); } - public virtual string GetNameof(DMObject dmObject, DMProc proc) { - throw new CompileAbortException(Location, "nameof: requires a var, proc reference, or type path"); - } + /// + /// Gets the canonical name of the expression if it exists. + /// + /// The name of the expression, or null if it does not have one. + public virtual string? GetNameof(DMObject dmObject, DMProc proc) => null; /// /// Determines whether the expression returns an ambiguous path. diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 71ef94c9ef..fce59748f1 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -1,5 +1,7 @@ -using DMCompiler.DM.Visitors; +using DMCompiler.Bytecode; using DMCompiler.Compiler.DM; +using DMCompiler.DM.Visitors; +using OpenDreamShared.Compiler; using OpenDreamShared.Dream; using OpenDreamShared.Dream.Procs; using OpenDreamShared.Json; @@ -7,17 +9,17 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using DMCompiler.Bytecode; -using OpenDreamShared.Compiler; namespace DMCompiler.DM { internal sealed class DMProc { public class LocalVariable { + public readonly string Name; public readonly int Id; public readonly bool IsParameter; public DreamPath? Type; - public LocalVariable(int id, bool isParameter, DreamPath? type) { + public LocalVariable(string name, int id, bool isParameter, DreamPath? type) { + Name = name; Id = id; IsParameter = isParameter; Type = type; @@ -27,7 +29,8 @@ public LocalVariable(int id, bool isParameter, DreamPath? type) { public sealed class LocalConstVariable : LocalVariable { public readonly Expressions.Constant Value; - public LocalConstVariable(int id, DreamPath? type, Expressions.Constant value) : base(id, false, type) { + public LocalConstVariable(string name, int id, DreamPath? type, Expressions.Constant value) + : base(name, id, false, type) { Value = value; } } @@ -192,10 +195,6 @@ public ProcDefinitionJson GetJsonRepresentation() { return procDefinition; } - public string GetLocalVarName(int index) { - return _localVariableNames[index].Add; - } - public void WaitFor(bool waitFor) { if (waitFor) { // "waitfor" is true by default @@ -227,7 +226,7 @@ public void AddParameter(string name, DMValueType valueType, DreamPath? type) { if (_parameters.ContainsKey(name)) { DMCompiler.Emit(WarningCode.DuplicateVariable, _astDefinition.Location, $"Duplicate argument \"{name}\""); } else { - _parameters.Add(name, new LocalVariable(_parameters.Count, true, type)); + _parameters.Add(name, new LocalVariable(name, _parameters.Count, true, type)); } } @@ -320,7 +319,7 @@ public bool TryAddLocalVariable(string name, DreamPath? type) { return false; int localVarId = AllocLocalVariable(name); - return _scopes.Peek().LocalVariables.TryAdd(name, new LocalVariable(localVarId, false, type)); + return _scopes.Peek().LocalVariables.TryAdd(name, new LocalVariable(name, localVarId, false, type)); } public bool TryAddLocalConstVariable(string name, DreamPath? type, Expressions.Constant value) { @@ -328,7 +327,7 @@ public bool TryAddLocalConstVariable(string name, DreamPath? type, Expressions.C return false; int localVarId = AllocLocalVariable(name); - return _scopes.Peek().LocalVariables.TryAdd(name, new LocalConstVariable(localVarId, type, value)); + return _scopes.Peek().LocalVariables.TryAdd(name, new LocalConstVariable(name, localVarId, type, value)); } public LocalVariable? GetLocalVariable(string name) { diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index 1a6c68108a..8705f03ce2 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -1,10 +1,10 @@ -using OpenDreamShared.Compiler; +using DMCompiler.Bytecode; using DMCompiler.Compiler.DM; +using OpenDreamShared.Compiler; using OpenDreamShared.Dream; using OpenDreamShared.Json; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using DMCompiler.Bytecode; namespace DMCompiler.DM.Expressions { // "abc[d]" @@ -591,19 +591,6 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { } } - // nameof(x) - sealed class Nameof : DMExpression { - private readonly DMExpression _expr; - - public Nameof(Location location, DMExpression expr) : base(location) { - _expr = expr; - } - - public override void EmitPushValue(DMObject dmObject, DMProc proc) { - proc.PushString(_expr.GetNameof(dmObject, proc)); - } - } - // call(...)(...) sealed class CallStatement : DMExpression { private readonly DMExpression _a; // Procref, Object, LibName @@ -644,6 +631,15 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { proc.PushType(dmObject.Id); } } + + public override string? GetNameof(DMObject dmObject, DMProc proc) { + if (dmObject.Path.LastElement != null) { + return dmObject.Path.LastElement; + } + + DMCompiler.Emit(WarningCode.BadArgument, Location, "Attempt to get nameof(__TYPE__) in global proc"); + return null; + } } // __PROC__ @@ -655,6 +651,8 @@ public ProcType(Location location) public override void EmitPushValue(DMObject dmObject, DMProc proc) { proc.PushProc(proc.Id); } + + public override string GetNameof(DMObject dmObject, DMProc proc) => proc.Name; } internal class Sin : DMExpression { diff --git a/DMCompiler/DM/Expressions/Constant.cs b/DMCompiler/DM/Expressions/Constant.cs index aa65c8b80f..598944ab41 100644 --- a/DMCompiler/DM/Expressions/Constant.cs +++ b/DMCompiler/DM/Expressions/Constant.cs @@ -523,9 +523,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { } } - public override string GetNameof(DMObject dmObject, DMProc proc) { - return Value.LastElement; - } + public override string? GetNameof(DMObject dmObject, DMProc proc) => Value.LastElement; public override bool IsTruthy() => true; diff --git a/DMCompiler/DM/Expressions/Dereference.cs b/DMCompiler/DM/Expressions/Dereference.cs index f3ccf66788..4cd0375af6 100644 --- a/DMCompiler/DM/Expressions/Dereference.cs +++ b/DMCompiler/DM/Expressions/Dereference.cs @@ -241,6 +241,13 @@ public void EmitPushIsSaved(DMObject dmObject, DMProc proc) { proc.AddLabel(endLabel); } + // BYOND says the nameof is invalid if the chain is not purely field operations + public override string? GetNameof(DMObject dmObject, DMProc proc) { + return _operations.All(op => op is FieldOperation) + ? ((FieldOperation)_operations[^1]).Identifier + : null; + } + public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) { var prevPath = _operations.Length == 1 ? _expression.Path : _operations[^2].Path; diff --git a/DMCompiler/DM/Expressions/LValue.cs b/DMCompiler/DM/Expressions/LValue.cs index 50bcf8390b..1ffc5c3e35 100644 --- a/DMCompiler/DM/Expressions/LValue.cs +++ b/DMCompiler/DM/Expressions/LValue.cs @@ -1,6 +1,6 @@ -using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; using OpenDreamShared.Compiler; +using System.Diagnostics.CodeAnalysis; namespace DMCompiler.DM.Expressions { abstract class LValue : DMExpression { @@ -26,8 +26,7 @@ public virtual void EmitPushInitial(DMObject dmObject, DMProc proc) { // global class Global : LValue { - public Global(Location location) - : base(location, null) { } + public Global(Location location) : base(location, null) { } public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode) { throw new CompileErrorException(Location, $"attempt to use `global` as a reference"); @@ -44,9 +43,7 @@ public override DMReference EmitReference(DMObject dmObject, DMProc proc, string return DMReference.Src; } - public override string GetNameof(DMObject dmObject, DMProc proc) { - return "src"; - } + public override string GetNameof(DMObject dmObject, DMProc proc) => "src"; } // usr @@ -58,6 +55,8 @@ public Usr(Location location) public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode) { return DMReference.Usr; } + + public override string GetNameof(DMObject dmObject, DMProc proc) => "usr"; } // args @@ -69,6 +68,8 @@ public Args(Location location) public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode) { return DMReference.Args; } + + public override string GetNameof(DMObject dmObject, DMProc proc) => "args"; } // Identifier of local variable @@ -104,9 +105,7 @@ public override void EmitPushInitial(DMObject dmObject, DMProc proc) { EmitPushValue(dmObject, proc); } - public override string GetNameof(DMObject dmObject, DMProc proc) { - return LocalVar.IsParameter ? proc.Parameters[LocalVar.Id] : proc.GetLocalVarName(LocalVar.Id); - } + public override string GetNameof(DMObject dmObject, DMProc proc) => LocalVar.Name; } // Identifier of field @@ -134,6 +133,8 @@ public override DMReference EmitReference(DMObject dmObject, DMProc proc, string return DMReference.CreateSrcField(Variable.Name); } + public override string GetNameof(DMObject dmObject, DMProc proc) => Variable.Name; + public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) { if (Variable.IsConst && Variable.Value != null) { return Variable.Value.TryAsConstant(out constant); @@ -191,5 +192,7 @@ public GlobalVars(Location location) public override void EmitPushValue(DMObject dmObject, DMProc proc) { proc.PushGlobalVars(); } + + public override string GetNameof(DMObject dmObject, DMProc proc) => "vars"; } } diff --git a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs index 1e0e9e7da7..c50ccba9d5 100644 --- a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs @@ -248,7 +248,7 @@ public static DMExpression BuildExpression(DMASTExpression expression, DMObject case DMASTInitial initial: return new Initial(initial.Location, BuildExpression(initial.Expression, dmObject, proc, inferredPath)); case DMASTNameof nameof: - return new Nameof(nameof.Location, BuildExpression(nameof.Expression, dmObject, proc, inferredPath)); + return BuildNameof(nameof, dmObject, proc, inferredPath); case DMASTExpressionIn expressionIn: return new In(expressionIn.Location, BuildExpression(expressionIn.Value, dmObject, proc, inferredPath), @@ -720,6 +720,17 @@ private static DMExpression BuildDimensionalList(DMASTDimensionalList list, DMOb return new DimensionalList(list.Location, sizes); } + // nameof(x) + private static DMExpression BuildNameof(DMASTNameof nameof, DMObject dmObject, DMProc proc, DreamPath? inferredPath) { + var expr = BuildExpression(nameof.Expression, dmObject, proc, inferredPath); + if (expr.GetNameof(dmObject, proc) is { } name) { + return new Expressions.String(nameof.Location, name); + } + + DMCompiler.Emit(WarningCode.BadArgument, nameof.Location, "nameof() requires a var, proc reference, or type path"); + return new Null(nameof.Location); + } + private static DMExpression BuildNewList(DMASTNewList newList, DMObject dmObject, DMProc proc, DreamPath? inferredPath) { DMExpression[] expressions = new DMExpression[newList.Parameters.Length]; From cb333702ced05588881ffd7bc97f64df8f5e0c26 Mon Sep 17 00:00:00 2001 From: ike709 Date: Mon, 22 Jan 2024 20:59:59 -0700 Subject: [PATCH 55/64] Initial world.Error() implementation (#1598) Co-authored-by: ike709 Co-authored-by: wixoa --- OpenDreamRuntime/DreamManager.cs | 15 +++++++++- OpenDreamRuntime/DreamThread.cs | 20 ++++++++++--- OpenDreamRuntime/Objects/DreamObjectTree.cs | 2 ++ .../Objects/Types/DreamObjectException.cs | 29 +++++++++++++++++++ OpenDreamRuntime/Procs/DMProc.cs | 4 +++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 OpenDreamRuntime/Objects/Types/DreamObjectException.cs diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 2bd61973cc..dad94d06e4 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -303,9 +303,22 @@ public DreamValue LocateRef(string refString) { return DreamValue.Null; } - public void HandleException(Exception e) { + public void HandleException(Exception e, string msg = "", string file = "", int line = 0) { + if (string.IsNullOrEmpty(msg)) { // Just print the C# exception if we don't override the message + msg = e.Message; + } + LastDMException = e; OnException?.Invoke(this, e); + + // Invoke world.Error() + var obj =_objectTree.CreateObject(_objectTree.Exception); + obj.Name = e.Message; + obj.Description = msg; + obj.Line = line; + obj.File = file; + + WorldInstance.SpawnProc("Error", usr: null, new DreamValue(obj)); } } diff --git a/OpenDreamRuntime/DreamThread.cs b/OpenDreamRuntime/DreamThread.cs index 50f7307c8f..8e1ea7bb71 100644 --- a/OpenDreamRuntime/DreamThread.cs +++ b/OpenDreamRuntime/DreamThread.cs @@ -343,8 +343,7 @@ public void AppendStackTrace(StringBuilder builder) { { builder.Append("(init)..."); } - else - { + else { _current.AppendStackFrame(builder); } builder.AppendLine(); @@ -368,7 +367,7 @@ public void HandleException(Exception exception) { _current?.Cancel(); var dreamMan = IoCManager.Resolve(); - dreamMan.HandleException(exception); + StringBuilder builder = new(); builder.AppendLine($"Exception occurred: {exception.Message}"); @@ -381,7 +380,20 @@ public void HandleException(Exception exception) { builder.AppendLine(exception.ToString()); builder.AppendLine(); - dreamMan.WriteWorldLog(builder.ToString(), LogLevel.Error); + var msg = builder.ToString(); + + // TODO: Defining world.Error() causes byond to no longer print exceptions to the log unless ..() is called + dreamMan.WriteWorldLog(msg, LogLevel.Error); + + // Instantiate an /exception and invoke world.Error() + string file = string.Empty; + int line = 0; + if(_current is DMProcState dmProc) { // TODO: Cope with the other ProcStates + var source = dmProc.GetCurrentSource(); + file = source.Item1; + line = source.Item2; + } + dreamMan.HandleException(exception, msg, file, line); IoCManager.Resolve().HandleException(this, exception); } diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index dcb884708f..eaba0d09a8 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -162,6 +162,8 @@ public DreamObject CreateObject(TreeEntry type) { throw new Exception("Cannot create objects of type /client"); if (type.ObjectDefinition.IsSubtypeOf(Turf)) throw new Exception("New turfs must be created by the map manager"); + if (type.ObjectDefinition.IsSubtypeOf(Exception)) + return new DreamObjectException(type.ObjectDefinition); return new DreamObject(type.ObjectDefinition); } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectException.cs b/OpenDreamRuntime/Objects/Types/DreamObjectException.cs new file mode 100644 index 0000000000..a45c09017a --- /dev/null +++ b/OpenDreamRuntime/Objects/Types/DreamObjectException.cs @@ -0,0 +1,29 @@ +namespace OpenDreamRuntime.Objects.Types; + +public sealed class DreamObjectException(DreamObjectDefinition objectDefinition) : DreamObject(objectDefinition) { + public string Name = string.Empty; + public string Description = string.Empty; + public string File = string.Empty; + public int Line = 0; + + //TODO: Match the format of BYOND exceptions since SS13 does splittext and other things to extract data from exceptions + + protected override bool TryGetVar(string varName, out DreamValue value) { + switch (varName) { + case "name": + value = new DreamValue(Name); + return true; + case "desc": + value = new DreamValue(Description); + return true; + case "file": + value = new DreamValue(File); + return true; + case "line": + value = new DreamValue(Line); + return true; + default: + return base.TryGetVar(varName, out value); + } + } +} diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index 72cce21987..35e834d453 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -437,6 +437,10 @@ public override void AppendStackFrame(StringBuilder builder) { builder.Append(Proc.GetSourceAtOffset(_pc - 1).Line); } + public (string, int) GetCurrentSource() { + return Proc.GetSourceAtOffset(_pc - 1); + } + public void Jump(int position) { _pc = position; } From 5625d11ed59fa3fb7fd14c3d9f15ff76610cb562 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 23 Jan 2024 13:27:28 -0500 Subject: [PATCH 56/64] Remove DMCompiler's dependency on OpenDreamShared (#1626) --- .github/workflows/compiler-test.yml | 4 +- Content.Tests/DummyDreamMapManager.cs | 2 +- DMCompiler/Compiler/CompilerError.cs | 143 ++++ DMCompiler/Compiler/DM/DMAST.cs | 13 +- DMCompiler/Compiler/DM/DMASTHelper.cs | 261 ------- DMCompiler/Compiler/DM/DMLexer.cs | 20 +- DMCompiler/Compiler/DM/DMParser.cs | 6 +- DMCompiler/Compiler/DM/DMParserHelper.cs | 683 +++++++++--------- DMCompiler/Compiler/DMM/DMMParser.cs | 242 +++---- DMCompiler/Compiler/DMPreprocessor/DMMacro.cs | 1 - .../Compiler/DMPreprocessor/DMPreprocessor.cs | 16 +- .../DMPreprocessor/DMPreprocessorLexer.cs | 1 - .../DMPreprocessor/DMPreprocessorParser.cs | 1 - DMCompiler/Compiler/Lexer.cs | 133 ++++ DMCompiler/Compiler/Parser.cs | 116 +++ DMCompiler/Compiler/Token.cs | 162 +++++ DMCompiler/DM/DMExpression.cs | 248 ++++--- DMCompiler/DM/DMObject.cs | 319 ++++---- DMCompiler/DM/DMObjectTree.cs | 355 ++++----- DMCompiler/DM/DMProc.cs | 66 +- .../Dream => DMCompiler/DM}/DMValueType.cs | 6 +- DMCompiler/DM/DMVariable.cs | 75 +- DMCompiler/DM/Expressions/Binary.cs | 18 +- DMCompiler/DM/Expressions/Builtins.cs | 5 +- DMCompiler/DM/Expressions/Constant.cs | 4 +- DMCompiler/DM/Expressions/Dereference.cs | 3 +- DMCompiler/DM/Expressions/LValue.cs | 4 +- DMCompiler/DM/Expressions/Procs.cs | 4 +- DMCompiler/DM/Expressions/Ternary.cs | 79 +- DMCompiler/DM/Expressions/Unary.cs | 1 - .../Dream => DMCompiler/DM}/MatrixOpcodes.cs | 2 +- .../Procs => DMCompiler/DM}/ProcAttributes.cs | 5 +- DMCompiler/DM/Visitors/DMExpressionBuilder.cs | 5 +- DMCompiler/DM/Visitors/DMObjectBuilder.cs | 3 +- DMCompiler/DM/Visitors/DMProcBuilder.cs | 6 +- DMCompiler/DMCompiler.cs | 10 +- DMCompiler/DMCompiler.csproj | 4 - .../Json/DreamCompiledJson.cs | 4 +- DMCompiler/Json/DreamMapJson.cs | 54 ++ DMCompiler/Json/DreamObjectJson.cs | 30 + DMCompiler/Json/DreamProcJson.cs | 38 + DMCompiler/Location.cs | 33 + DMCompiler/Program.cs | 2 - DMDisassembler/DMProc.cs | 84 +-- DMDisassembler/DMType.cs | 4 +- DMDisassembler/Program.cs | 2 +- OpenDreamClient/Interface/DMF/DMFLexer.cs | 190 ++--- OpenDreamClient/Interface/DMF/DMFParser.cs | 159 ++-- .../Interface/DreamInterfaceManager.cs | 62 +- .../Interface/Prompts/AlertWindow.cs | 4 +- .../Interface/Prompts/ColorPrompt.cs | 6 +- .../Interface/Prompts/InputWindow.cs | 4 +- .../Interface/Prompts/ListPrompt.cs | 4 +- .../Interface/Prompts/MessagePrompt.cs | 4 +- .../Interface/Prompts/NumberPrompt.cs | 4 +- .../Interface/Prompts/PromptWindow.cs | 6 +- .../Interface/Prompts/TextPrompt.cs | 4 +- OpenDreamClient/Program.cs | 14 +- OpenDreamRuntime/DreamConnection.cs | 44 +- OpenDreamRuntime/DreamManager.cs | 2 +- OpenDreamRuntime/DreamMapManager.cs | 2 +- OpenDreamRuntime/DreamThread.cs | 6 +- OpenDreamRuntime/Objects/DreamObjectTree.cs | 3 +- OpenDreamRuntime/Procs/AsyncNativeProc.cs | 3 +- OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 2 +- OpenDreamRuntime/Procs/DMProc.cs | 10 +- .../Procs/Native/DreamProcNativeRoot.cs | 1 + OpenDreamRuntime/Procs/NativeProc.cs | 3 +- OpenDreamRuntime/Procs/ProcDecoder.cs | 18 +- OpenDreamShared/Compiler/AST.cs | 5 - OpenDreamShared/Compiler/ASTVisitor.cs | 5 - OpenDreamShared/Compiler/CompilerError.cs | 146 ---- OpenDreamShared/Compiler/Lexer.cs | 138 ---- OpenDreamShared/Compiler/Location.cs | 38 - OpenDreamShared/Compiler/Parser.cs | 120 --- OpenDreamShared/Compiler/Token.cs | 180 ----- OpenDreamShared/Dream/ATOMType.cs | 8 - OpenDreamShared/Dream/ColorHelpers.cs | 74 +- OpenDreamShared/Dream/DreamValueType.cs | 32 + OpenDreamShared/Json/DreamMapJson.cs | 64 -- OpenDreamShared/Json/DreamObjectJson.cs | 30 - OpenDreamShared/Json/DreamProcJson.cs | 39 - OpenDreamShared/Network/Messages/MsgPrompt.cs | 47 +- .../Network/Messages/MsgPromptResponse.cs | 74 +- 84 files changed, 2245 insertions(+), 2587 deletions(-) create mode 100644 DMCompiler/Compiler/CompilerError.cs delete mode 100644 DMCompiler/Compiler/DM/DMASTHelper.cs create mode 100644 DMCompiler/Compiler/Lexer.cs create mode 100644 DMCompiler/Compiler/Parser.cs create mode 100644 DMCompiler/Compiler/Token.cs rename {OpenDreamShared/Dream => DMCompiler/DM}/DMValueType.cs (81%) rename {OpenDreamShared/Dream => DMCompiler/DM}/MatrixOpcodes.cs (91%) rename {OpenDreamShared/Dream/Procs => DMCompiler/DM}/ProcAttributes.cs (78%) rename {OpenDreamShared => DMCompiler}/Json/DreamCompiledJson.cs (90%) create mode 100644 DMCompiler/Json/DreamMapJson.cs create mode 100644 DMCompiler/Json/DreamObjectJson.cs create mode 100644 DMCompiler/Json/DreamProcJson.cs create mode 100644 DMCompiler/Location.cs delete mode 100644 OpenDreamShared/Compiler/AST.cs delete mode 100644 OpenDreamShared/Compiler/ASTVisitor.cs delete mode 100644 OpenDreamShared/Compiler/CompilerError.cs delete mode 100644 OpenDreamShared/Compiler/Lexer.cs delete mode 100644 OpenDreamShared/Compiler/Location.cs delete mode 100644 OpenDreamShared/Compiler/Parser.cs delete mode 100644 OpenDreamShared/Compiler/Token.cs delete mode 100644 OpenDreamShared/Dream/ATOMType.cs create mode 100644 OpenDreamShared/Dream/DreamValueType.cs delete mode 100644 OpenDreamShared/Json/DreamMapJson.cs delete mode 100644 OpenDreamShared/Json/DreamObjectJson.cs delete mode 100644 OpenDreamShared/Json/DreamProcJson.cs diff --git a/.github/workflows/compiler-test.yml b/.github/workflows/compiler-test.yml index 95e70668c1..2323980fb3 100644 --- a/.github/workflows/compiler-test.yml +++ b/.github/workflows/compiler-test.yml @@ -29,9 +29,9 @@ jobs: with: dotnet-version: 8.0.100 - name: Install dependencies - run: dotnet restore main/OpenDream.sln + run: dotnet restore main/DMCompiler/DMCompiler.csproj - name: Build - run: dotnet build main/OpenDream.sln --configuration Release --no-restore /m + run: dotnet build main/DMCompiler/DMCompiler.csproj --configuration Release --no-restore /m - name: Compile TestGame run: main\bin\DMCompiler\DMCompiler.exe main\TestGame\environment.dme - name: Checkout Modified /tg/station diff --git a/Content.Tests/DummyDreamMapManager.cs b/Content.Tests/DummyDreamMapManager.cs index 8a4180808b..904f3a87f8 100644 --- a/Content.Tests/DummyDreamMapManager.cs +++ b/Content.Tests/DummyDreamMapManager.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using DMCompiler.Json; using OpenDreamRuntime; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; using OpenDreamShared.Dream; -using OpenDreamShared.Json; using Robust.Shared.GameObjects; using Robust.Shared.Maths; diff --git a/DMCompiler/Compiler/CompilerError.cs b/DMCompiler/Compiler/CompilerError.cs new file mode 100644 index 0000000000..e2281c5931 --- /dev/null +++ b/DMCompiler/Compiler/CompilerError.cs @@ -0,0 +1,143 @@ +using System; + +namespace DMCompiler.Compiler; + +/// +/// All values should be unique. +/// +public enum WarningCode { + // 0 - 999 are reserved for giving codes to fatal errors which cannot reasonably be demoted to a warning/notice/disable. + Unknown = 0, + BadToken = 1, + BadDirective = 10, + BadExpression = 11, + MissingExpression = 12, + BadLabel = 19, + InvalidReference = 50, + BadArgument = 100, + InvalidArgumentKey = 101, + ArglistOnlyArgument = 102, + HardReservedKeyword = 200, // For keywords that CANNOT be un-reserved. + ItemDoesntExist = 404, + DanglingOverride = 405, + StaticOverride = 406, + // ReSharper disable once InconsistentNaming + IAmATeaPot = 418, // TODO: Implement the HTCPC protocol for OD + HardConstContext = 500, + WriteToConstant = 501, + InvalidInclusion = 900, + + // 1000 - 1999 are reserved for preprocessor configuration. + FileAlreadyIncluded = 1000, + MissingIncludedFile = 1001, + MisplacedDirective = 1100, + UndefineMissingDirective = 1101, + DefinedMissingParen = 1150, + ErrorDirective = 1200, + WarningDirective = 1201, + MiscapitalizedDirective = 1300, + + // 2000 - 2999 are reserved for compiler configuration of actual behaviour. + SoftReservedKeyword = 2000, // For keywords that SHOULD be reserved, but don't have to be. 'null' and 'defined', for instance + DuplicateVariable = 2100, + DuplicateProcDefinition = 2101, + TooManyArguments = 2200, + PointlessParentCall = 2205, + PointlessBuiltinCall = 2206, // For pointless calls to issaved() or initial() + SuspiciousMatrixCall = 2207, // Calling matrix() with seemingly the wrong arguments + FallbackBuiltinArgument = 2208, // A builtin (sin(), cos(), etc) with an invalid/fallback argument + MalformedRange = 2300, + InvalidRange = 2301, + InvalidSetStatement = 2302, + InvalidOverride = 2303, + DanglingVarType = 2401, // For types inferred by a particular var definition and nowhere else, that ends up not existing (not forced-fatal because BYOND doesn't always error) + MissingInterpolatedExpression = 2500, // A text macro is missing a required interpolated expression + AmbiguousResourcePath = 2600, + + // 3000 - 3999 are reserved for stylistic configuration. + EmptyBlock = 3100, + EmptyProc = 3101, + UnsafeClientAccess = 3200, + SuspiciousSwitchCase = 3201, // "else if" cases are actually valid DM, they just spontaneously end the switch context and begin an if-else ladder within the else case of the switch + AssignmentInConditional = 3202, + + // 4000 - 4999 are reserved for runtime configuration. (TODO: Runtime doesn't know about configs yet!) +} + +public enum ErrorLevel { + //When this warning is emitted: + Disabled, // Nothing happens. + Notice, // Nothing happens unless the user provides a '--wall' argument. + Warning, // A warning is always emitted. + Error // An error is always emitted. +} + +/// +/// Stores the location and message of a notice/warning/error. +/// +public struct CompilerEmission { + public ErrorLevel Level; + public WarningCode Code; + public Location Location; + public string Message; + + public CompilerEmission(ErrorLevel level, Location? location, string message) { + Level = level; + Code = WarningCode.Unknown; + Location = location ?? Location.Unknown; + Message = message; + } + + public CompilerEmission(ErrorLevel level, WarningCode code, Location? location, string message) { + Level = level; + Code = code; + Location = location ?? Location.Unknown; + Message = message; + } + + public override string ToString() => Level switch { + ErrorLevel.Disabled => "", + ErrorLevel.Notice => $"Notice OD{(int)Code:d4} at {Location.ToString()}: {Message}", + ErrorLevel.Warning => $"Warning OD{(int)Code:d4} at {Location.ToString()}: {Message}", + ErrorLevel.Error => $"Error OD{(int)Code:d4} at {Location.ToString()}: {Message}", + _ => "", + }; +} + +[Obsolete("This is not a desirable way for the compiler to emit an error. Use CompileAbortException or ForceError() if it needs to be fatal, or an DMCompiler.Emit() otherwise.")] +public class CompileErrorException : Exception { + public CompilerEmission Error; + + public CompileErrorException(CompilerEmission error) : base(error.Message) { + Error = error; + } + + public CompileErrorException(Location location, string message) : base(message) { + Error = new CompilerEmission(ErrorLevel.Error, location, message); + } + + public CompileErrorException(string message) { + Error = new CompilerEmission(ErrorLevel.Error, Location.Unknown, message); + } +} + + +/// +/// Represents an internal compiler error that should cause parsing for a particular block to cease.
+/// This should be ideally used for exceptions that are the fault of the compiler itself,
+/// like an abnormal state being reached or something. +///
+public sealed class CompileAbortException : CompileErrorException { + public CompileAbortException(CompilerEmission error) : base(error) { + } + + public CompileAbortException(Location location, string message) : base(location, message) { + } + + public CompileAbortException(string message) : base(message) { + } +} + +public sealed class UnknownIdentifierException(Location location, string identifierName) : CompileErrorException(location, $"Unknown identifier \"{identifierName}\"") { + public string IdentifierName = identifierName; +} diff --git a/DMCompiler/Compiler/DM/DMAST.cs b/DMCompiler/Compiler/DM/DMAST.cs index 59f0f5c246..971cd63230 100644 --- a/DMCompiler/Compiler/DM/DMAST.cs +++ b/DMCompiler/Compiler/DM/DMAST.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; +using DMCompiler.DM; namespace DMCompiler.Compiler.DM { - public interface DMASTVisitor : ASTVisitor { + public interface DMASTVisitor { public void VisitFile(DMASTFile file) { throw new NotImplementedException(); } @@ -532,12 +531,8 @@ public void VisitCallableGlobalProc(DMASTCallableGlobalProc globalIdentifier) { } } - public abstract class DMASTNode : ASTNode { - protected DMASTNode(Location location) { - Location = location; - } - - public readonly Location Location; + public abstract class DMASTNode(Location location) { + public readonly Location Location = location; public abstract void Visit(DMASTVisitor visitor); } diff --git a/DMCompiler/Compiler/DM/DMASTHelper.cs b/DMCompiler/Compiler/DM/DMASTHelper.cs deleted file mode 100644 index 0040d2ef66..0000000000 --- a/DMCompiler/Compiler/DM/DMASTHelper.cs +++ /dev/null @@ -1,261 +0,0 @@ - -using System; -using System.Text.RegularExpressions; -using System.Linq; -using System.Collections.Generic; -using System.Reflection; -using OpenDreamShared.Dream; -using OpenDreamShared.Compiler; - -namespace DMCompiler.Compiler.DM { - - public static partial class DMAST { - public sealed class TopLevelTraveler { - public Action VisitDefine; - - public void Travel(DMASTFile root) { - Travel(root.BlockInner); - } - public void Travel(DMASTBlockInner block) { - if (block == null) { return; } - if (block.Statements != null) { - foreach (var stmt in block.Statements) { - Travel((dynamic)stmt); - } - } - } - - public void Travel(DMASTObjectDefinition objdef) { - Travel(objdef.InnerBlock); - VisitDefine(objdef); - } - - public void Travel(DMASTObjectVarDefinition vardef) { - VisitDefine(vardef); - } - public void Travel(DMASTObjectVarOverride vardef) { - VisitDefine(vardef); - } - - public void Travel(DMASTProcDefinition procdef) { - VisitDefine(procdef); - } - } - public sealed class ASTHasher { - public static string Hash(DMASTObjectDefinition objdef) { - return $"OD-{objdef.Path}"; - } - - public static string Hash(DMASTObjectVarDefinition vardef) { - return $"OVD-{vardef.ObjectPath}-{vardef.Name}"; - } - public static string Hash(DMASTObjectVarOverride vardef) { - return $"OVO-{vardef.ObjectPath}-{vardef.VarName}"; - } - - public static string Hash(DMASTProcDefinition procdef) { - return $"PD-{procdef.ObjectPath}-{procdef.IsOverride}-{procdef.Name}"; - } - - public Dictionary> nodes = new(); - - public List GetNode(DMASTNode node) { - if (nodes.TryGetValue(Hash((dynamic)node), out List rval)) { - return rval; - } - else { return null; } - } - public void HashFile(DMASTFile node) { - var traveler = new TopLevelTraveler(); - traveler.VisitDefine = HashDefine; - traveler.Travel(node); - } - - public DMASTProcDefinition GetProcByPath(string path) { - var h = Hash(new DMASTProcDefinition(Location.Unknown, new DreamPath(path), new DMASTDefinitionParameter[0], null)); - return nodes[h][0] as DMASTProcDefinition; - } - public void HashDefine(DMASTNode node) { - var h = Hash((dynamic)node); - if (nodes.ContainsKey(h)) { - nodes[h].Add(node); - } - else { - nodes.Add(h, new List { node }); - } - } - } - - public sealed class Labeler { - public Dictionary labels = new(); - public int label_i = 0; - - public void Add(object obj) { - if (labels.ContainsKey(obj)) { - return; - } - labels[obj] = label_i++; - } - - public int? GetLabel(object obj) { - if (labels.TryGetValue(obj, out var i)) { - return i; - } - return null; - } - - } - - public class ObjectPrinter { - public List tostring_types = new() { - typeof(string), - typeof(int), - typeof(float), - typeof(bool), - typeof(char) - }; - public List recurse_types = new() { }; - public List ignore_types = new() { }; - - public sealed class ObjectTraveler { } - - public void Print(object node, System.IO.TextWriter print, int depth = 0, int max_depth = 9999, Labeler labeler = null) { - if (depth > max_depth) { - return; - } - string pad = new string(' ', 4 + 2 * depth); - string line = ""; - if (node == null) { - print.WriteLine(pad + "null"); - return; - } - else { - line += node.GetType().Name + " "; - } - List<(string, object)> recurse = new(); - - if (node is string s) { - print.Write(Regex.Escape(node.ToString())); - return; - } - if (node.GetType().IsArray) { - var a = (Array)node; - var i = 0; - foreach (var e in a) { - recurse.Add((i.ToString(), e)); - i += 1; - } - } - foreach (var field in node.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic)) { - Type ty = Nullable.GetUnderlyingType(field.FieldType); - if (ty == null) { - ty = field.FieldType; - } - if (ignore_types.Contains(ty)) { - continue; - } - - var v = field.GetValue(node); - bool is_recurse = false; - foreach (var rt in recurse_types) { - if (ty.IsAssignableTo(rt)) { - recurse.Add((field.Name, v)); - is_recurse = true; - break; - } - } - if (is_recurse) { - continue; - } - if (ty.IsArray) { - recurse.Add((field.Name, v)); - } - else if (v == null) { - line += field.Name + "=" + "null" + " "; - } - else if (tostring_types.Contains(ty)) { - line += field.Name + "=" + v.ToString() + " "; - } - else { - throw new Exception("unknown field type " + ty.ToString()); - } - } - var label_i = labeler?.GetLabel(node); - if (label_i != null) { - print.WriteLine(label_i.ToString().PadRight(4 + 2 * depth) + line); - } - else { - print.WriteLine("".PadRight(4 + 2 * depth) + line); - } - foreach (var r in recurse) { - if (r.Item2 != null) { - print.WriteLine(pad + "->" + r.Item1); - Print(r.Item2, print, depth + 1, max_depth, labeler); - } - else { - print.WriteLine(pad + "->" + r.Item1 + "=null"); - } - } - } - } - - // Example usage: - // var hasher = new DMAST.ASTHasher(); - // hasher.HashFile(astFile); - // var proc = hasher.GetProcByPath("/datum/browser/proc/get_header"); - // new DMAST.DMASTNodePrinter().Print(proc, Console.Out); - public sealed class DMASTNodePrinter : ObjectPrinter { - public DMASTNodePrinter() { - tostring_types.AddRange( new Type[] { typeof(DMValueType), typeof(DreamPath), typeof(DreamPath.PathType) } ); - recurse_types.AddRange( new Type[] { typeof(DMASTNode), typeof(DMASTCallable), typeof(VarDeclInfo) } ); - ignore_types.Add(typeof(Location)); - } - } - - public delegate void CompareResult(DMASTNode n_l, DMASTNode n_r, string s); - public static bool Compare(DMASTNode node_l, DMASTNode node_r, CompareResult cr) { - if (node_l == null || node_r == null) { - if (node_r == node_l) { return true; } - cr(node_l, node_r, "null mismatch"); - return false; - } - - if (node_l.GetType() != node_r.GetType()) { cr(node_l, node_r, "type mismatch"); return false; } - - List compared = new(); - DMASTNode[] subnodes_l = node_l.LeafNodes().ToArray(); - DMASTNode[] subnodes_r = node_r.LeafNodes().ToArray(); - - if (subnodes_l.Length != subnodes_r.Length) { cr(node_l, node_r, "nodes length mismatch " + subnodes_l.Length + " " + subnodes_r.Length); return false; } - - for (var i = 0; i < subnodes_l.Length; i++) { - Compare(subnodes_l[i], subnodes_r[i], cr); - compared.Add(subnodes_l); - } - - foreach (var field in node_l.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)) { - if (compared.Contains(field.GetValue(node_l))) { - continue; - } - //TODO non-node type field checking goes here - } - - return true; - } - public static IEnumerable LeafNodes(this DMASTNode node) { - foreach (var field in node.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic)) { - var value = field.GetValue(node); - if (value == null) { continue; } - if (field.FieldType.IsAssignableTo(typeof(DMASTNode))) { - yield return value as DMASTNode; - } - else if (field.FieldType.IsArray && field.FieldType.GetElementType().IsAssignableTo(typeof(DMASTNode))) { - var field_value = value as DMASTNode[]; - foreach (var subnode in field_value) { - yield return subnode; - } - } - } - } - } -} diff --git a/DMCompiler/Compiler/DM/DMLexer.cs b/DMCompiler/Compiler/DM/DMLexer.cs index cf2193855a..a9a9ca2248 100644 --- a/DMCompiler/Compiler/DM/DMLexer.cs +++ b/DMCompiler/Compiler/DM/DMLexer.cs @@ -2,13 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Text; -using OpenDreamShared.Compiler; namespace DMCompiler.Compiler.DM; public sealed class DMLexer : TokenLexer { - // NOTE: .NET still needs you to pass the capacity size to generate the most optimal code, so update it when you change these values - public static readonly List ValidEscapeSequences = new(38) { + public static readonly List ValidEscapeSequences = [ "icon", "Roman", "roman", "The", "the", @@ -28,14 +26,14 @@ public sealed class DMLexer : TokenLexer { "bold", "b", "italic", "..." - }; + ]; private static readonly StringBuilder TokenTextBuilder = new(); - private static readonly List ValidIdentifierComponents = new(2) { + private static readonly List ValidIdentifierComponents = [ TokenType.DM_Preproc_Identifier, TokenType.DM_Preproc_Number - }; + ]; // NOTE: .NET still needs you to pass the capacity size to generate the most optimal code, so update it when you change these values private static readonly Dictionary Keywords = new(25) { @@ -267,10 +265,8 @@ protected override Token ParseNextToken() { TokenTextBuilder.Append(GetCurrent().Text); } while (ValidIdentifierComponents.Contains(Advance().Type) && !AtEndOfSource); - string identifierText = TokenTextBuilder.ToString(); - var tokenType = Keywords.TryGetValue(identifierText, out TokenType keywordType) - ? keywordType - : TokenType.DM_Identifier; + var identifierText = TokenTextBuilder.ToString(); + var tokenType = Keywords.GetValueOrDefault(identifierText, TokenType.DM_Identifier); token = CreateToken(tokenType, identifierText); break; @@ -279,9 +275,9 @@ protected override Token ParseNextToken() { Advance(); string text = preprocToken.Text; - if (text == "1.#INF" || text == "1#INF") { + if (text is "1.#INF" or "1#INF") { token = CreateToken(TokenType.DM_Float, text, float.PositiveInfinity); - } else if (text == "1.#IND" || text == "1#IND") { + } else if (text is "1.#IND" or "1#IND") { token = CreateToken(TokenType.DM_Float, text, float.NaN); } else if (text.StartsWith("0x") && int.TryParse(text.Substring(2), NumberStyles.HexNumber, null, out int intValue)) { token = CreateToken(TokenType.DM_Integer, text, intValue); diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 2bad848edb..ce7c4a96aa 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -1,15 +1,13 @@ using DMCompiler.Compiler.DMPreprocessor; -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; using System; using System.Collections.Generic; using System.Linq; -using String = System.String; +using DMCompiler.DM; namespace DMCompiler.Compiler.DM { public partial class DMParser : Parser { private DreamPath _currentPath = DreamPath.Root; - private bool _allowVarDeclExpression = false; + private bool _allowVarDeclExpression; private static readonly TokenType[] AssignTypes = { TokenType.DM_Equals, diff --git a/DMCompiler/Compiler/DM/DMParserHelper.cs b/DMCompiler/Compiler/DM/DMParserHelper.cs index 9acef9c83e..747401479f 100644 --- a/DMCompiler/Compiler/DM/DMParserHelper.cs +++ b/DMCompiler/Compiler/DM/DMParserHelper.cs @@ -1,389 +1,388 @@ -using OpenDreamShared.Compiler; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using DMCompiler.Bytecode; -namespace DMCompiler.Compiler.DM { - public partial class DMParser { - /// - /// A special override of Error() since, for DMParser, we know we are in a compilation context and can make use of error codes. - /// - /// - /// Should only be called AFTER has built up its list of pragma configurations. - /// - /// True if this will raise an error, false if not. You can use this return value to help improve error emission around this (depending on how permissive we're being) - protected bool Error(WarningCode code, string message) { - ErrorLevel level = DMCompiler.CodeToLevel(code); - if (Emissions.Count < MAX_EMISSIONS_RECORDED) - Emissions.Add(new CompilerEmission(level, code, Current().Location, message)); - return level == ErrorLevel.Error; - } +namespace DMCompiler.Compiler.DM; + +public partial class DMParser { + /// + /// A special override of Error() since, for DMParser, we know we are in a compilation context and can make use of error codes. + /// + /// + /// Should only be called AFTER has built up its list of pragma configurations. + /// + /// True if this will raise an error, false if not. You can use this return value to help improve error emission around this (depending on how permissive we're being) + protected bool Error(WarningCode code, string message) { + ErrorLevel level = DMCompiler.CodeToLevel(code); + if (Emissions.Count < MAX_EMISSIONS_RECORDED) + Emissions.Add(new CompilerEmission(level, code, Current().Location, message)); + return level == ErrorLevel.Error; + } - /// - [Obsolete("This is not a desirable way for DMParser to emit an error, as errors should emit an error code and not cause unnecessary throws. Use DMParser's overrides of this method, instead.")] - new protected void Error(string message, bool throwException = true) { - base.Error(message, throwException); - } + /// + [Obsolete("This is not a desirable way for DMParser to emit an error, as errors should emit an error code and not cause unnecessary throws. Use DMParser's overrides of this method, instead.")] + protected new void Error(string message, bool throwException = true) { + base.Error(message, throwException); + } - protected bool PeekDelimiter() { - return Current().Type == TokenType.Newline || Current().Type == TokenType.DM_Semicolon; - } + protected bool PeekDelimiter() { + return Current().Type == TokenType.Newline || Current().Type == TokenType.DM_Semicolon; + } - protected void LocateNextStatement() { - while (!PeekDelimiter() && Current().Type != TokenType.DM_Dedent) { - Advance(); + protected void LocateNextStatement() { + while (!PeekDelimiter() && Current().Type != TokenType.DM_Dedent) { + Advance(); - if (Current().Type == TokenType.EndOfFile) { - break; - } + if (Current().Type == TokenType.EndOfFile) { + break; } } + } - protected void LocateNextTopLevel() { - do { - LocateNextStatement(); - - Delimiter(); - while (Current().Type == TokenType.DM_Dedent) { - Advance(); - } - - if (Current().Type == TokenType.EndOfFile) break; - } while (((DMLexer)_lexer).CurrentIndentation() != 0); + protected void LocateNextTopLevel() { + do { + LocateNextStatement(); Delimiter(); - } - - private void ConsumeRightParenthesis() { - //A missing right parenthesis has to subtract 1 from the lexer's bracket nesting counter - //To keep indentation working correctly - if (!Check(TokenType.DM_RightParenthesis)) { - ((DMLexer)_lexer).BracketNesting--; - Error("Expected ')'"); + while (Current().Type == TokenType.DM_Dedent) { + Advance(); } - } - private void ConsumeRightBracket() { - //Similar to ConsumeRightParenthesis() - if (!Check(TokenType.DM_RightBracket)) { - ((DMLexer)_lexer).BracketNesting--; - Error("Expected ']'"); - } - } + if (Current().Type == TokenType.EndOfFile) break; + } while (((DMLexer)_lexer).CurrentIndentation() != 0); - /// Small helper function for , for macros that require a preceding expression in the string. - /// if error occurs. - private bool CheckInterpolation(Location loc, bool hasSeenNonRefInterpolation, List? interpolationValues, string mack) { - if (interpolationValues == null || interpolationValues.Count == 0) { - DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, loc, $"Macro \"\\{mack}\" requires preceding interpolated expression"); - return true; - } + Delimiter(); + } - if(!hasSeenNonRefInterpolation) { // More elaborate error for a more elaborate situation - DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, loc, $"Macro \"\\{mack}\" requires preceding interpolated expression that is not a reference"); - return true; - } + private void ConsumeRightParenthesis() { + //A missing right parenthesis has to subtract 1 from the lexer's bracket nesting counter + //To keep indentation working correctly + if (!Check(TokenType.DM_RightParenthesis)) { + ((DMLexer)_lexer).BracketNesting--; + Error("Expected ')'"); + } + } - return false; + private void ConsumeRightBracket() { + //Similar to ConsumeRightParenthesis() + if (!Check(TokenType.DM_RightBracket)) { + ((DMLexer)_lexer).BracketNesting--; + Error("Expected ']'"); } + } - private bool TryConvertUtfCodeToString(ReadOnlySpan input, ref StringBuilder stringBuilder) { - int utf32Code; - if (!int.TryParse(input, style: System.Globalization.NumberStyles.HexNumber, provider: null, out utf32Code)) { - return false; - } - stringBuilder.Append(char.ConvertFromUtf32(utf32Code)); + /// Small helper function for , for macros that require a preceding expression in the string. + /// if error occurs. + private bool CheckInterpolation(Location loc, bool hasSeenNonRefInterpolation, List? interpolationValues, string mack) { + if (interpolationValues == null || interpolationValues.Count == 0) { + DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, loc, $"Macro \"\\{mack}\" requires preceding interpolated expression"); return true; } - /// - /// Handles parsing of Tokens of type or a series of tokens starting with .
- /// (Shunted into a helper because this is a quite long and arduous block of code) - ///
- /// Either a or a . - private DMASTExpression ExpressionFromString() { - // The actual text (but includes special codepoints for macros and markers for where interps go) - StringBuilder stringBuilder = new(); - List? interpolationValues = null; - StringFormatEncoder.FormatSuffix currentInterpolationType = StringFormatEncoder.InterpolationDefault; - string? usedPrefixMacro = null; // A string holding the name of the last prefix macro (\the, \a etc.) used, for error presentation poipoises - bool hasSeenNonRefInterpolation = false; - var tokenLoc = Current().Location; - - while (true) { - Token currentToken = Current(); - Advance(); - - string tokenValue = (string)currentToken.Value; + if(!hasSeenNonRefInterpolation) { // More elaborate error for a more elaborate situation + DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, loc, $"Macro \"\\{mack}\" requires preceding interpolated expression that is not a reference"); + return true; + } - // If an interpolation comes after this, ignore the last character (always '[') - int iterateLength = currentToken.Type is TokenType.DM_StringBegin or TokenType.DM_StringMiddle - ? tokenValue.Length - 1 - : tokenValue.Length; + return false; + } - // If an interpolation came before this, ignore the first character (always ']') - int iterateBegin = currentToken.Type is TokenType.DM_StringMiddle or TokenType.DM_StringEnd ? 1 : 0; + private bool TryConvertUtfCodeToString(ReadOnlySpan input, ref StringBuilder stringBuilder) { + int utf32Code; + if (!int.TryParse(input, style: System.Globalization.NumberStyles.HexNumber, provider: null, out utf32Code)) { + return false; + } + stringBuilder.Append(char.ConvertFromUtf32(utf32Code)); + return true; + } - stringBuilder.EnsureCapacity(stringBuilder.Length + iterateLength - iterateBegin); + /// + /// Handles parsing of Tokens of type or a series of tokens starting with .
+ /// (Shunted into a helper because this is a quite long and arduous block of code) + ///
+ /// Either a or a . + private DMASTExpression ExpressionFromString() { + // The actual text (but includes special codepoints for macros and markers for where interps go) + StringBuilder stringBuilder = new(); + List? interpolationValues = null; + StringFormatEncoder.FormatSuffix currentInterpolationType = StringFormatEncoder.InterpolationDefault; + string? usedPrefixMacro = null; // A string holding the name of the last prefix macro (\the, \a etc.) used, for error presentation poipoises + bool hasSeenNonRefInterpolation = false; + var tokenLoc = Current().Location; + + while (true) { + Token currentToken = Current(); + Advance(); + + string tokenValue = (string)currentToken.Value; + + // If an interpolation comes after this, ignore the last character (always '[') + int iterateLength = currentToken.Type is TokenType.DM_StringBegin or TokenType.DM_StringMiddle + ? tokenValue.Length - 1 + : tokenValue.Length; + + // If an interpolation came before this, ignore the first character (always ']') + int iterateBegin = currentToken.Type is TokenType.DM_StringMiddle or TokenType.DM_StringEnd ? 1 : 0; + + stringBuilder.EnsureCapacity(stringBuilder.Length + iterateLength - iterateBegin); + + for (int i = iterateBegin; i < iterateLength; i++) { + char c = tokenValue[i]; + + switch (c) { + case '\\': { + string escapeSequence = string.Empty; + + if (i == tokenValue.Length - 1) { + Error("Invalid escape sequence"); + } - for (int i = iterateBegin; i < iterateLength; i++) { - char c = tokenValue[i]; + c = tokenValue[++i]; - switch (c) { - case '\\': { - string escapeSequence = string.Empty; + int? utfCodeDigitsExpected = null; + switch (c) { + case 'x': + utfCodeDigitsExpected = 2; break; + case 'u': + utfCodeDigitsExpected = 4; break; + case 'U': + utfCodeDigitsExpected = 6; break; + } - if (i == tokenValue.Length - 1) { - Error("Invalid escape sequence"); + if (utfCodeDigitsExpected.HasValue) { + i++; + int utfCodeLength = Math.Min(utfCodeDigitsExpected.Value, tokenValue.Length - i); + var utfCode = tokenValue.AsSpan(i, utfCodeLength); + if (utfCodeLength < utfCodeDigitsExpected.Value || !TryConvertUtfCodeToString(utfCode, ref stringBuilder)) { + Error($"Invalid Unicode macro \"\\{c}{utfCode}\""); } - - c = tokenValue[++i]; - - int? utfCodeDigitsExpected = null; - switch (c) { - case 'x': - utfCodeDigitsExpected = 2; break; - case 'u': - utfCodeDigitsExpected = 4; break; - case 'U': - utfCodeDigitsExpected = 6; break; + i += utfCodeLength - 1; // -1, cause we have i++ in the current 'for' expression + } else if (char.IsLetter(c)) { + while (i < tokenValue.Length && char.IsLetter(tokenValue[i])) { + escapeSequence += tokenValue[i++]; } - - if (utfCodeDigitsExpected.HasValue) { - i++; - int utfCodeLength = Math.Min(utfCodeDigitsExpected.Value, tokenValue.Length - i); - var utfCode = tokenValue.AsSpan(i, utfCodeLength); - if (utfCodeLength < utfCodeDigitsExpected.Value || !TryConvertUtfCodeToString(utfCode, ref stringBuilder)) { - Error($"Invalid Unicode macro \"\\{c}{utfCode}\""); - } - i += utfCodeLength - 1; // -1, cause we have i++ in the current 'for' expression - } else if (char.IsLetter(c)) { - while (i < tokenValue.Length && char.IsLetter(tokenValue[i])) { - escapeSequence += tokenValue[i++]; - } - i--; - - bool skipSpaces = false; - bool consumeSpaceCharacter = false; - switch (escapeSequence) { - case "Proper": // Users can have a little case-insensitivity, as a treat - case "Improper": - Warning($"Escape sequence \"\\{escapeSequence}\" should not be capitalized. Coercing macro to \"\\{escapeSequence.ToLower()}"); - escapeSequence = escapeSequence.ToLower(); - goto case "proper"; // Fallthrough! - case "proper": - case "improper": - if (stringBuilder.Length != 0) { - Error($"Escape sequence \"\\{escapeSequence}\" must come at the beginning of the string"); - } - - skipSpaces = true; - if(escapeSequence == "proper") - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Proper)); - else - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Improper)); - break; - case "roman": - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerRoman)); - break; - case "Roman": - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperRoman)); - break; - - case "ref": - // usedPrefixMacro = true; -- while ref is indeed a prefix macro, it DOES NOT ERROR if it fails to find what it's supposed to /ref. - // TODO: Actually care about this when we add --noparity - currentInterpolationType = StringFormatEncoder.FormatSuffix.ReferenceOfValue; break; - - case "The": - usedPrefixMacro = "The"; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperDefiniteArticle)); - break; - case "the": - usedPrefixMacro = "the"; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerDefiniteArticle)); - break; - - case "A": - case "An": - usedPrefixMacro = escapeSequence; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle)); - break; - case "a": - case "an": - usedPrefixMacro = escapeSequence; - consumeSpaceCharacter = true; - currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerIndefiniteArticle)); - break; - - case "He": - case "She": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperSubjectPronoun)); - break; - case "he": - case "she": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerSubjectPronoun)); - break; - - case "His": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "His")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessiveAdjective)); - break; - case "his": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "his")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessiveAdjective)); - break; - - case "Him": // BYOND errors here but lets be nice! - Warning("\"\\Him\" is not an available text macro. Coercing macro into \"\\him\""); - goto case "him"; // Fallthrough! - case "him": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "him")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ObjectPronoun)); - break; - - case "Her": - case "her": - Error("\"Her\" is a grammatically ambiguous pronoun. Use \\him or \\his instead"); - break; - - case "himself": - case "herself": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ReflexivePronoun)); - break; - - case "Hers": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "Hers")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessivePronoun)); - break; - case "hers": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "hers")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessivePronoun)); - break; - //Plurals, ordinals, etc - //(things that hug, as a suffix, the [] that they reference) - case "s": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "s")) break; - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.PluralSuffix)); - break; - case "th": - if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "th")) break; - // TODO: this should error if not DIRECTLY after an expression ([]\s vs []AA\s) - stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.OrdinalIndicator)); - break; - default: - if (escapeSequence.StartsWith("n")) { - stringBuilder.Append('\n'); - stringBuilder.Append(escapeSequence.Skip(1).ToArray()); - } else if (escapeSequence.StartsWith("t")) { - stringBuilder.Append('\t'); - stringBuilder.Append(escapeSequence.Skip(1).ToArray()); - } else if (!DMLexer.ValidEscapeSequences.Contains(escapeSequence)) { // This only exists to allow unimplements to fallthrough w/o a direct error - Error($"Invalid escape sequence \"\\{escapeSequence}\""); - } - - break; - } - - if (skipSpaces) { - // Note that some macros in BYOND require a single/zero space between them and the [] - // This doesn't replicate that - while (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; - } - - if(consumeSpaceCharacter) { - if (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; - } + i--; + + bool skipSpaces = false; + bool consumeSpaceCharacter = false; + switch (escapeSequence) { + case "Proper": // Users can have a little case-insensitivity, as a treat + case "Improper": + Warning($"Escape sequence \"\\{escapeSequence}\" should not be capitalized. Coercing macro to \"\\{escapeSequence.ToLower()}"); + escapeSequence = escapeSequence.ToLower(); + goto case "proper"; // Fallthrough! + case "proper": + case "improper": + if (stringBuilder.Length != 0) { + Error($"Escape sequence \"\\{escapeSequence}\" must come at the beginning of the string"); + } + + skipSpaces = true; + if(escapeSequence == "proper") + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Proper)); + else + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.Improper)); + break; + case "roman": + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerRoman)); + break; + case "Roman": + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperRoman)); + break; + + case "ref": + // usedPrefixMacro = true; -- while ref is indeed a prefix macro, it DOES NOT ERROR if it fails to find what it's supposed to /ref. + // TODO: Actually care about this when we add --noparity + currentInterpolationType = StringFormatEncoder.FormatSuffix.ReferenceOfValue; break; + + case "The": + usedPrefixMacro = "The"; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperDefiniteArticle)); + break; + case "the": + usedPrefixMacro = "the"; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerDefiniteArticle)); + break; + + case "A": + case "An": + usedPrefixMacro = escapeSequence; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle)); + break; + case "a": + case "an": + usedPrefixMacro = escapeSequence; + consumeSpaceCharacter = true; + currentInterpolationType = StringFormatEncoder.FormatSuffix.StringifyNoArticle; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerIndefiniteArticle)); + break; + + case "He": + case "She": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperSubjectPronoun)); + break; + case "he": + case "she": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerSubjectPronoun)); + break; + + case "His": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "His")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessiveAdjective)); + break; + case "his": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "his")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessiveAdjective)); + break; + + case "Him": // BYOND errors here but lets be nice! + Warning("\"\\Him\" is not an available text macro. Coercing macro into \"\\him\""); + goto case "him"; // Fallthrough! + case "him": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "him")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ObjectPronoun)); + break; + + case "Her": + case "her": + Error("\"Her\" is a grammatically ambiguous pronoun. Use \\him or \\his instead"); + break; + + case "himself": + case "herself": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, escapeSequence)) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.ReflexivePronoun)); + break; + + case "Hers": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "Hers")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.UpperPossessivePronoun)); + break; + case "hers": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "hers")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.LowerPossessivePronoun)); + break; + //Plurals, ordinals, etc + //(things that hug, as a suffix, the [] that they reference) + case "s": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "s")) break; + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.PluralSuffix)); + break; + case "th": + if (CheckInterpolation(tokenLoc, hasSeenNonRefInterpolation, interpolationValues, "th")) break; + // TODO: this should error if not DIRECTLY after an expression ([]\s vs []AA\s) + stringBuilder.Append(StringFormatEncoder.Encode(StringFormatEncoder.FormatSuffix.OrdinalIndicator)); + break; + default: + if (escapeSequence.StartsWith("n")) { + stringBuilder.Append('\n'); + stringBuilder.Append(escapeSequence.Skip(1).ToArray()); + } else if (escapeSequence.StartsWith("t")) { + stringBuilder.Append('\t'); + stringBuilder.Append(escapeSequence.Skip(1).ToArray()); + } else if (!DMLexer.ValidEscapeSequences.Contains(escapeSequence)) { // This only exists to allow unimplements to fallthrough w/o a direct error + Error($"Invalid escape sequence \"\\{escapeSequence}\""); + } + + break; } - else { - escapeSequence += c; - switch (escapeSequence) { - case "[": - case "]": - case "<": - case ">": - case "\"": - case "'": - case "\\": - case " ": - case ".": - stringBuilder.Append(escapeSequence); - break; - default: //Unimplemented escape sequence - Error("Invalid escape sequence \"\\" + escapeSequence + "\""); - break; - } + + if (skipSpaces) { + // Note that some macros in BYOND require a single/zero space between them and the [] + // This doesn't replicate that + while (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; } - break; + if(consumeSpaceCharacter) { + if (i < tokenValue.Length - 1 && tokenValue[i + 1] == ' ') i++; + } } - default: { - stringBuilder.Append(c); - break; + else { + escapeSequence += c; + switch (escapeSequence) { + case "[": + case "]": + case "<": + case ">": + case "\"": + case "'": + case "\\": + case " ": + case ".": + stringBuilder.Append(escapeSequence); + break; + default: //Unimplemented escape sequence + Error("Invalid escape sequence \"\\" + escapeSequence + "\""); + break; + } } + + break; + } + default: { + stringBuilder.Append(c); + break; } } + } - // We've parsed the text of this piece of string, what happens next depends on what token this was - switch (currentToken.Type) { - case TokenType.DM_ConstantString: // Constant singular piece of string, return here - if (usedPrefixMacro != null) // FIXME: \the should not compiletime here, instead becoming a tab character followed by "he", when in parity mode - DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, tokenLoc, - $"Macro \"\\{usedPrefixMacro}\" requires interpolated expression"); - - return new DMASTConstantString(currentToken.Location, stringBuilder.ToString()); - case TokenType.DM_StringBegin: - case TokenType.DM_StringMiddle: // An interpolation is coming up, collect the expression - interpolationValues ??= new(1); - - Whitespace(); - if (Current().Type is TokenType.DM_StringMiddle or TokenType.DM_StringEnd) { // Empty interpolation - interpolationValues.Add(null); - } else { - var interpolatedExpression = Expression(); - if (interpolatedExpression == null) - DMCompiler.Emit(WarningCode.MissingExpression, Current().Location, - "Expected an embedded expression"); - - // The next token should be the next piece of the string, error if not - if (Current().Type is not TokenType.DM_StringMiddle and not TokenType.DM_StringEnd) { - DMCompiler.Emit(WarningCode.BadExpression, Current().Location, - "Expected end of the embedded expression"); - - while (Current().Type is not TokenType.DM_StringMiddle and not TokenType.DM_StringEnd - and not TokenType.EndOfFile) { - Advance(); - } + // We've parsed the text of this piece of string, what happens next depends on what token this was + switch (currentToken.Type) { + case TokenType.DM_ConstantString: // Constant singular piece of string, return here + if (usedPrefixMacro != null) // FIXME: \the should not compiletime here, instead becoming a tab character followed by "he", when in parity mode + DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, tokenLoc, + $"Macro \"\\{usedPrefixMacro}\" requires interpolated expression"); + + return new DMASTConstantString(currentToken.Location, stringBuilder.ToString()); + case TokenType.DM_StringBegin: + case TokenType.DM_StringMiddle: // An interpolation is coming up, collect the expression + interpolationValues ??= new(1); + + Whitespace(); + if (Current().Type is TokenType.DM_StringMiddle or TokenType.DM_StringEnd) { // Empty interpolation + interpolationValues.Add(null); + } else { + var interpolatedExpression = Expression(); + if (interpolatedExpression == null) + DMCompiler.Emit(WarningCode.MissingExpression, Current().Location, + "Expected an embedded expression"); + + // The next token should be the next piece of the string, error if not + if (Current().Type is not TokenType.DM_StringMiddle and not TokenType.DM_StringEnd) { + DMCompiler.Emit(WarningCode.BadExpression, Current().Location, + "Expected end of the embedded expression"); + + while (Current().Type is not TokenType.DM_StringMiddle and not TokenType.DM_StringEnd + and not TokenType.EndOfFile) { + Advance(); } - interpolationValues.Add(interpolatedExpression); } + interpolationValues.Add(interpolatedExpression); + } - hasSeenNonRefInterpolation |= currentInterpolationType != StringFormatEncoder.FormatSuffix.ReferenceOfValue; - stringBuilder.Append(StringFormatEncoder.Encode(currentInterpolationType)); - currentInterpolationType = StringFormatEncoder.InterpolationDefault; - break; - case TokenType.DM_StringEnd: // End of a string with interpolated values, return here - if(currentInterpolationType != StringFormatEncoder.InterpolationDefault) { // this implies a prefix tried to modify a [] that never ended up existing after it - DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, tokenLoc, - $"Macro \"\\{usedPrefixMacro}\" must precede an interpolated expression"); - } + hasSeenNonRefInterpolation |= currentInterpolationType != StringFormatEncoder.FormatSuffix.ReferenceOfValue; + stringBuilder.Append(StringFormatEncoder.Encode(currentInterpolationType)); + currentInterpolationType = StringFormatEncoder.InterpolationDefault; + break; + case TokenType.DM_StringEnd: // End of a string with interpolated values, return here + if(currentInterpolationType != StringFormatEncoder.InterpolationDefault) { // this implies a prefix tried to modify a [] that never ended up existing after it + DMCompiler.Emit(WarningCode.MissingInterpolatedExpression, tokenLoc, + $"Macro \"\\{usedPrefixMacro}\" must precede an interpolated expression"); + } - return new DMASTStringFormat(tokenLoc, stringBuilder.ToString(), interpolationValues!.ToArray()); - } + return new DMASTStringFormat(tokenLoc, stringBuilder.ToString(), interpolationValues!.ToArray()); } } } diff --git a/DMCompiler/Compiler/DMM/DMMParser.cs b/DMCompiler/Compiler/DMM/DMMParser.cs index e91136570e..88b2aecc1d 100644 --- a/DMCompiler/Compiler/DMM/DMMParser.cs +++ b/DMCompiler/Compiler/DMM/DMMParser.cs @@ -1,177 +1,171 @@ using System; -using OpenDreamShared.Compiler; -using OpenDreamShared.Json; using DMCompiler.DM; using System.Collections.Generic; using DMCompiler.Compiler.DM; +using DMCompiler.Json; -namespace DMCompiler.Compiler.DMM { - internal sealed class DMMParser : DMParser { - private int _cellNameLength = -1; - private readonly int _zOffset; - private readonly HashSet _skippedTypes = new(); +namespace DMCompiler.Compiler.DMM; - public DMMParser(DMLexer lexer, int zOffset) : base(lexer) { - _zOffset = zOffset; - } +internal sealed class DMMParser(DMLexer lexer, int zOffset) : DMParser(lexer) { + private int _cellNameLength = -1; + private readonly HashSet _skippedTypes = new(); - public DreamMapJson ParseMap() { - DreamMapJson map = new DreamMapJson(); + public DreamMapJson ParseMap() { + DreamMapJson map = new DreamMapJson(); - _cellNameLength = -1; + _cellNameLength = -1; - bool parsing = true; - while (parsing) { - CellDefinitionJson? cellDefinition = ParseCellDefinition(); - if (cellDefinition != null) { - if (_cellNameLength == -1) _cellNameLength = cellDefinition.Name.Length; - else if (cellDefinition.Name.Length != _cellNameLength) Error("Invalid cell definition name"); + bool parsing = true; + while (parsing) { + CellDefinitionJson? cellDefinition = ParseCellDefinition(); + if (cellDefinition != null) { + if (_cellNameLength == -1) _cellNameLength = cellDefinition.Name.Length; + else if (cellDefinition.Name.Length != _cellNameLength) Error("Invalid cell definition name"); - map.CellDefinitions.Add(cellDefinition.Name, cellDefinition); - } + map.CellDefinitions.Add(cellDefinition.Name, cellDefinition); + } - MapBlockJson? mapBlock = ParseMapBlock(); - if (mapBlock != null) { - int maxX = mapBlock.X + mapBlock.Width - 1; - int maxY = mapBlock.Y + mapBlock.Height - 1; - if (map.MaxX < maxX) map.MaxX = maxX; - if (map.MaxY < maxY) map.MaxY = maxY; - if (map.MaxZ < mapBlock.Z) map.MaxZ = mapBlock.Z; + MapBlockJson? mapBlock = ParseMapBlock(); + if (mapBlock != null) { + int maxX = mapBlock.X + mapBlock.Width - 1; + int maxY = mapBlock.Y + mapBlock.Height - 1; + if (map.MaxX < maxX) map.MaxX = maxX; + if (map.MaxY < maxY) map.MaxY = maxY; + if (map.MaxZ < mapBlock.Z) map.MaxZ = mapBlock.Z; - map.Blocks.Add(mapBlock); - } - - if (cellDefinition == null && mapBlock == null) parsing = false; + map.Blocks.Add(mapBlock); } - Consume(TokenType.EndOfFile, "Expected EOF"); - return map; + if (cellDefinition == null && mapBlock == null) parsing = false; } - public CellDefinitionJson? ParseCellDefinition() { - Token currentToken = Current(); + Consume(TokenType.EndOfFile, "Expected EOF"); + return map; + } - if (Check(TokenType.DM_ConstantString)) { - Consume(TokenType.DM_Equals, "Expected '='"); - Consume(TokenType.DM_LeftParenthesis, "Expected '('"); + public CellDefinitionJson? ParseCellDefinition() { + Token currentToken = Current(); - CellDefinitionJson cellDefinition = new CellDefinitionJson((string)currentToken.Value); - DMASTPath? objectType = Path(); - while (objectType != null) { - bool skipType = !DMObjectTree.TryGetTypeId(objectType.Path, out int typeId); - if (skipType && _skippedTypes.Add(objectType.Path)) { - Warning($"Skipping type '{objectType.Path}'"); - } + if (Check(TokenType.DM_ConstantString)) { + Consume(TokenType.DM_Equals, "Expected '='"); + Consume(TokenType.DM_LeftParenthesis, "Expected '('"); - MapObjectJson mapObject = new MapObjectJson(typeId); + CellDefinitionJson cellDefinition = new CellDefinitionJson((string)currentToken.Value); + DMASTPath? objectType = Path(); + while (objectType != null) { + bool skipType = !DMObjectTree.TryGetTypeId(objectType.Path, out int typeId); + if (skipType && _skippedTypes.Add(objectType.Path)) { + Warning($"Skipping type '{objectType.Path}'"); + } - if (Check(TokenType.DM_LeftCurlyBracket)) { - DMASTStatement? statement = Statement(requireDelimiter: false); + MapObjectJson mapObject = new MapObjectJson(typeId); - while (statement != null) { - DMASTObjectVarOverride? varOverride = statement as DMASTObjectVarOverride; - if (varOverride == null) Error("Expected a var override"); - if (!varOverride.ObjectPath.Equals(DreamPath.Root)) DMCompiler.ForcedError(statement.Location, $"Invalid var name '{varOverride.VarName}' in DMM on type {objectType.Path}"); - DMExpression value = DMExpression.Create(DMObjectTree.GetDMObject(objectType.Path, false), null, varOverride.Value); - if (!value.TryAsJsonRepresentation(out var valueJson)) DMCompiler.ForcedError(statement.Location, $"Failed to serialize value to json ({value})"); + if (Check(TokenType.DM_LeftCurlyBracket)) { + DMASTStatement? statement = Statement(requireDelimiter: false); - if(!mapObject.AddVarOverride(varOverride.VarName, valueJson)) { - DMCompiler.ForcedWarning(statement.Location, $"Duplicate var override '{varOverride.VarName}' in DMM on type {objectType.Path}"); - } + while (statement != null) { + DMASTObjectVarOverride? varOverride = statement as DMASTObjectVarOverride; + if (varOverride == null) Error("Expected a var override"); + if (!varOverride.ObjectPath.Equals(DreamPath.Root)) DMCompiler.ForcedError(statement.Location, $"Invalid var name '{varOverride.VarName}' in DMM on type {objectType.Path}"); + DMExpression value = DMExpression.Create(DMObjectTree.GetDMObject(objectType.Path, false), null, varOverride.Value); + if (!value.TryAsJsonRepresentation(out var valueJson)) DMCompiler.ForcedError(statement.Location, $"Failed to serialize value to json ({value})"); - if (Check(TokenType.DM_Semicolon)) { - statement = Statement(requireDelimiter: false); - } else { - statement = null; - } + if(!mapObject.AddVarOverride(varOverride.VarName, valueJson)) { + DMCompiler.ForcedWarning(statement.Location, $"Duplicate var override '{varOverride.VarName}' in DMM on type {objectType.Path}"); } - Consume(TokenType.DM_RightCurlyBracket, "Expected '}'"); - } - - if (!skipType) { - if (objectType.Path.IsDescendantOf(DreamPath.Turf)) { - cellDefinition.Turf = mapObject; - } else if (objectType.Path.IsDescendantOf(DreamPath.Area)) { - cellDefinition.Area = mapObject; + if (Check(TokenType.DM_Semicolon)) { + statement = Statement(requireDelimiter: false); } else { - cellDefinition.Objects.Add(mapObject); + statement = null; } } - if (Check(TokenType.DM_Comma)) { - objectType = Path(); + Consume(TokenType.DM_RightCurlyBracket, "Expected '}'"); + } + + if (!skipType) { + if (objectType.Path.IsDescendantOf(DreamPath.Turf)) { + cellDefinition.Turf = mapObject; + } else if (objectType.Path.IsDescendantOf(DreamPath.Area)) { + cellDefinition.Area = mapObject; } else { - objectType = null; + cellDefinition.Objects.Add(mapObject); } } - Consume(TokenType.DM_RightParenthesis, "Expected ')'"); - return cellDefinition; + if (Check(TokenType.DM_Comma)) { + objectType = Path(); + } else { + objectType = null; + } } - return null; + Consume(TokenType.DM_RightParenthesis, "Expected ')'"); + return cellDefinition; } - public MapBlockJson? ParseMapBlock() { - (int X, int Y, int Z)? coordinates = Coordinates(); + return null; + } - if (coordinates.HasValue) { - MapBlockJson mapBlock = new MapBlockJson(coordinates.Value.X, coordinates.Value.Y, coordinates.Value.Z); + public MapBlockJson? ParseMapBlock() { + (int X, int Y, int Z)? coordinates = Coordinates(); - Consume(TokenType.DM_Equals, "Expected '='"); - Token blockStringToken = Current(); - Consume(TokenType.DM_ConstantString, "Expected a constant string"); + if (coordinates.HasValue) { + MapBlockJson mapBlock = new MapBlockJson(coordinates.Value.X, coordinates.Value.Y, coordinates.Value.Z); - string blockString = (string)blockStringToken.Value; - List lines = new(blockString.Split("\n", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)); + Consume(TokenType.DM_Equals, "Expected '='"); + Token blockStringToken = Current(); + Consume(TokenType.DM_ConstantString, "Expected a constant string"); - mapBlock.Height = lines.Count; - for (int y = 1; y <= lines.Count; y++) { - string line = lines[y - 1]; - int width = (line.Length / _cellNameLength); + string blockString = (string)blockStringToken.Value; + List lines = new(blockString.Split("\n", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)); - if (mapBlock.Width < width) mapBlock.Width = width; - if ((line.Length % _cellNameLength) != 0) Error("Invalid map block row"); + mapBlock.Height = lines.Count; + for (int y = 1; y <= lines.Count; y++) { + string line = lines[y - 1]; + int width = (line.Length / _cellNameLength); - for (int x = 1; x <= width; x++) { - string cell = line.Substring((x - 1) * _cellNameLength, _cellNameLength); + if (mapBlock.Width < width) mapBlock.Width = width; + if ((line.Length % _cellNameLength) != 0) Error("Invalid map block row"); - mapBlock.Cells.Add(cell); - } - } + for (int x = 1; x <= width; x++) { + string cell = line.Substring((x - 1) * _cellNameLength, _cellNameLength); - return mapBlock; - } else { - return null; + mapBlock.Cells.Add(cell); + } } - } - private (int X, int Y, int Z)? Coordinates() { - if (Check(TokenType.DM_LeftParenthesis)) { - DMASTConstantInteger? x = Constant() as DMASTConstantInteger; - if (x == null) Error("Expected an integer"); - Consume(TokenType.DM_Comma, "Expected ','"); - DMASTConstantInteger? y = Constant() as DMASTConstantInteger; - if (y == null) Error("Expected an integer"); - Consume(TokenType.DM_Comma, "Expected ','"); - DMASTConstantInteger? z = Constant() as DMASTConstantInteger; - if (z == null) Error("Expected an integer"); - Consume(TokenType.DM_RightParenthesis, "Expected ')'"); - - return (x.Value, y.Value, z.Value + _zOffset); - } else { - return null; - } + return mapBlock; + } else { + return null; } + } - protected override Token Advance() { - //Throw out any newlines, indents, dedents, or whitespace - List ignoredTypes = new() { TokenType.Newline, TokenType.DM_Indent, TokenType.DM_Dedent, TokenType.DM_Whitespace }; - while (ignoredTypes.Contains(base.Advance().Type)) { - } + private (int X, int Y, int Z)? Coordinates() { + if (Check(TokenType.DM_LeftParenthesis)) { + DMASTConstantInteger? x = Constant() as DMASTConstantInteger; + if (x == null) Error("Expected an integer"); + Consume(TokenType.DM_Comma, "Expected ','"); + DMASTConstantInteger? y = Constant() as DMASTConstantInteger; + if (y == null) Error("Expected an integer"); + Consume(TokenType.DM_Comma, "Expected ','"); + DMASTConstantInteger? z = Constant() as DMASTConstantInteger; + if (z == null) Error("Expected an integer"); + Consume(TokenType.DM_RightParenthesis, "Expected ')'"); + + return (x.Value, y.Value, z.Value + zOffset); + } else { + return null; + } + } - return Current(); + protected override Token Advance() { + //Throw out any newlines, indents, dedents, or whitespace + List ignoredTypes = new() { TokenType.Newline, TokenType.DM_Indent, TokenType.DM_Dedent, TokenType.DM_Whitespace }; + while (ignoredTypes.Contains(base.Advance().Type)) { } + + return Current(); } } diff --git a/DMCompiler/Compiler/DMPreprocessor/DMMacro.cs b/DMCompiler/Compiler/DMPreprocessor/DMMacro.cs index 19d289aa21..c7841abae2 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMMacro.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMMacro.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using OpenDreamShared.Compiler; namespace DMCompiler.Compiler.DMPreprocessor; diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs index 7dc66d258e..75edb9219d 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessor.cs @@ -5,8 +5,6 @@ using System.IO; using System.Linq; using DMCompiler.Compiler.DM; -using OpenDreamShared.Compiler; -using Robust.Shared.Utility; namespace DMCompiler.Compiler.DMPreprocessor; @@ -414,7 +412,7 @@ private void HandleDefineDirective(Token defineToken) { if (macroTokens.Count > 0 && macroTokens[^1].Type == TokenType.DM_Preproc_Whitespace) { //Remove trailing whitespace - macroTokens.Pop(); + macroTokens.RemoveAt(macroTokens.Count - 1); } _defines[defineIdentifier.Text] = new DMMacro(parameters, macroTokens); @@ -476,7 +474,9 @@ private List GetLineOfTokens() /// If this Token is a macro, pushes all of its tokens onto the queue. /// true if the Token ended up meaning a macro sequence. private bool TryMacro(Token token) { - DebugTools.Assert(token.Type == TokenType.DM_Preproc_Identifier); // Check this before passing anything to this function. + if (token.Type != TokenType.DM_Preproc_Identifier) // Check this before passing anything to this function. + throw new ArgumentException("Given token must be a DM_Preproc_Identifier", nameof(token)); + if (!_defines.TryGetValue(token.Text, out DMMacro? macro)) { return false; } @@ -618,19 +618,21 @@ private void HandlePragmaDirective(Token pragmaDirective) { WarningCode warningCode; switch(warningNameToken.Type) { case TokenType.DM_Preproc_Identifier: { - if(!Enum.TryParse(warningNameToken.Text, out warningCode)) { + if (!Enum.TryParse(warningNameToken.Text, out warningCode)) { DMCompiler.Emit(WarningCode.BadDirective, warningNameToken.Location, $"Warning '{warningNameToken.PrintableText}' does not exist"); GetLineOfTokens(); // consume what's on this line and leave return; } + break; } case TokenType.DM_Preproc_Number: { - if(!int.TryParse(warningNameToken.Text, out var intValue)) { + if (!int.TryParse(warningNameToken.Text, out var intValue)) { DMCompiler.Emit(WarningCode.BadDirective, warningNameToken.Location, $"Warning OD{warningNameToken.PrintableText} does not exist"); GetLineOfTokens(); return; } + warningCode = (WarningCode)intValue; break; } @@ -640,7 +642,7 @@ private void HandlePragmaDirective(Token pragmaDirective) { return; } } - DebugTools.AssertNotNull(warningCode); + if((int)warningCode < 1000) { DMCompiler.Emit(WarningCode.BadDirective, warningNameToken.Location, $"Warning OD{(int)warningCode:d4} cannot be set - it must always be an error"); GetLineOfTokens(); diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs index 1b287d6d82..1b439c03d3 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs @@ -3,7 +3,6 @@ using System.IO; using System.Runtime.CompilerServices; using System.Text; -using OpenDreamShared.Compiler; namespace DMCompiler.Compiler.DMPreprocessor; diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs index f004e83f20..020033af96 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorParser.cs @@ -1,5 +1,4 @@ using DMCompiler.Compiler.DM; -using OpenDreamShared.Compiler; using System; using System.Collections.Generic; diff --git a/DMCompiler/Compiler/Lexer.cs b/DMCompiler/Compiler/Lexer.cs new file mode 100644 index 0000000000..8ef6f0e24a --- /dev/null +++ b/DMCompiler/Compiler/Lexer.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.IO; + +namespace DMCompiler.Compiler; + +public class Lexer { + public Location CurrentLocation { get; protected set; } + public string SourceName { get; protected set; } + public IEnumerable Source { get; protected set; } + public bool AtEndOfSource { get; protected set; } = false; + + protected Queue _pendingTokenQueue = new(); + + private readonly IEnumerator _sourceEnumerator; + private SourceType _current; + + protected Lexer(string sourceName, IEnumerable source) { + CurrentLocation = new Location(sourceName, 1, 0); + SourceName = sourceName; + Source = source; + if (source == null) + throw new FileNotFoundException("Source file could not be read: " + sourceName); + _sourceEnumerator = Source.GetEnumerator(); + } + + public Token GetNextToken() { + if (_pendingTokenQueue.Count > 0) + return _pendingTokenQueue.Dequeue(); + + Token nextToken = ParseNextToken(); + while (nextToken.Type == TokenType.Skip) nextToken = ParseNextToken(); + + if (_pendingTokenQueue.Count > 0) { + _pendingTokenQueue.Enqueue(nextToken); + return _pendingTokenQueue.Dequeue(); + } else { + return nextToken; + } + } + + protected virtual Token ParseNextToken() { + return CreateToken(TokenType.Unknown, GetCurrent()?.ToString() ?? string.Empty); + } + + protected Token CreateToken(TokenType type, string text, object? value = null) { + return new Token(type, text, CurrentLocation, value); + } + + protected Token CreateToken(TokenType type, char text, object? value = null) { + return CreateToken(type, char.ToString(text), value); + } + + protected virtual SourceType GetCurrent() { + return _current; + } + + protected virtual SourceType Advance() { + if (_sourceEnumerator.MoveNext()) { + _current = _sourceEnumerator.Current; + } else { + AtEndOfSource = true; + } + + return GetCurrent(); + } +} + +public class TextLexer : Lexer { + protected string _source; + protected int _currentPosition = 0; + + public TextLexer(string sourceName, string source) : base(sourceName, source) { + _source = source; + + Advance(); + } + + protected override Token ParseNextToken() { + char c = GetCurrent(); + + Token token; + switch (c) { + case '\n': token = CreateToken(TokenType.Newline, c); Advance(); break; + case '\0': token = CreateToken(TokenType.EndOfFile, c); Advance(); break; + default: token = CreateToken(TokenType.Unknown, c); break; + } + + return token; + } + + protected override char GetCurrent() { + if (AtEndOfSource) return '\0'; + else return base.GetCurrent(); + } + + protected override char Advance() { + if (GetCurrent() == '\n') { + CurrentLocation = new Location( + CurrentLocation.SourceFile, + CurrentLocation.Line + 1, + 1 + ); + } else { + CurrentLocation = new Location( + CurrentLocation.SourceFile, + CurrentLocation.Line, + CurrentLocation.Column + 1 + ); + } + + _currentPosition++; + return base.Advance(); + } +} + +public class TokenLexer : Lexer { + public TokenLexer(string sourceName, IEnumerable source) : base(sourceName, source) { + Advance(); + } + + protected override Token Advance() { + Token current = base.Advance(); + + //Warnings and errors go straight to output, no processing + while (current.Type is TokenType.Warning or TokenType.Error && !AtEndOfSource) { + _pendingTokenQueue.Enqueue(current); + current = base.Advance(); + } + + CurrentLocation = current.Location; + return current; + } +} diff --git a/DMCompiler/Compiler/Parser.cs b/DMCompiler/Compiler/Parser.cs new file mode 100644 index 0000000000..6f7eb39b8a --- /dev/null +++ b/DMCompiler/Compiler/Parser.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; + +namespace DMCompiler.Compiler; + +public class Parser { + /// Includes errors and warnings accumulated by this parser. + /// These initial capacities are arbitrary. We just assume there's a decent chance you'll get a handful of errors/warnings. + public List Emissions = new(8); + + protected Lexer _lexer; + private Token _currentToken; + private readonly Stack _tokenStack = new(1); + /// The maximum number of errors or warnings we'd ever place into . + protected const int MAX_EMISSIONS_RECORDED = 50_000_000; + + protected Parser(Lexer lexer) { + _lexer = lexer; + + Advance(); + } + + /// + /// Does not consume; this is simply a friendly getter. + /// + protected Token Current() { + return _currentToken; + } + + protected virtual Token Advance() { + if (_tokenStack.Count > 0) { + _currentToken = _tokenStack.Pop(); + } else { + _currentToken = _lexer.GetNextToken(); + + if (_currentToken.Type == TokenType.Error) { + Error((string)_currentToken.Value!, throwException: false); + Advance(); + } else if (_currentToken.Type == TokenType.Warning) { + Warning((string)_currentToken.Value!); + Advance(); + } + } + + return Current(); + } + + protected void ReuseToken(Token token) { + _tokenStack.Push(_currentToken); + _currentToken = token; + } + + protected bool Check(TokenType type) { + if (Current().Type == type) { + Advance(); + + return true; + } + + return false; + } + + protected bool Check(TokenType[] types) { + TokenType currentType = Current().Type; + foreach (TokenType type in types) { + if (currentType == type) { + Advance(); + + return true; + } + } + + return false; + } + + protected void Consume(TokenType type, string errorMessage) { + if (!Check(type)) { + Error(errorMessage); + } + } + + /// The that was found. + protected TokenType Consume(TokenType[] types, string errorMessage) { + foreach (TokenType type in types) { + if (Check(type)) return type; + } + + Error(errorMessage); + return TokenType.Unknown; + } + + /// + /// Emits an error discovered during parsing, optionally causing a throw. + /// + /// This implementation on does not make use of
+ /// since there are some parsers that aren't always in the compilation context, like the ones for DMF and DMM.
+ ///
+ protected void Error(string message, bool throwException = true) { + CompilerEmission error = new CompilerEmission(ErrorLevel.Error, _currentToken.Location, message); + + if(Emissions.Count < MAX_EMISSIONS_RECORDED) + Emissions.Add(error); + if (throwException) + throw new CompileErrorException(error); + } + + /// + /// Emits a warning discovered during parsing, optionally causing a throw. + /// + /// This implementation on does not make use of
+ /// since there are some parsers that aren't always in the compilation context, like the ones for DMF and DMM.
+ ///
+ protected void Warning(string message, Token? token = null) { + token ??= _currentToken; + Emissions.Add(new CompilerEmission(ErrorLevel.Warning, token?.Location, message)); + } +} diff --git a/DMCompiler/Compiler/Token.cs b/DMCompiler/Compiler/Token.cs new file mode 100644 index 0000000000..01deb45775 --- /dev/null +++ b/DMCompiler/Compiler/Token.cs @@ -0,0 +1,162 @@ +// ReSharper disable InconsistentNaming + +namespace DMCompiler.Compiler; + +// Must be : byte for ReadOnlySpan x = new TokenType[] { } to be intrinsic'd by the compiler. +public enum TokenType : byte { + //Base lexer + Error, + Warning, + Unknown, + Skip, //Internally skipped by the lexer + + //Text lexer + Newline, + EndOfFile, + + //DM Preprocessor + DM_Preproc_ConstantString, + DM_Preproc_Define, + DM_Preproc_Else, + DM_Preproc_EndIf, + DM_Preproc_Error, + DM_Preproc_Identifier, + DM_Preproc_If, + DM_Preproc_Ifdef, + DM_Preproc_Ifndef, + DM_Preproc_Elif, + DM_Preproc_Include, + DM_Preproc_LineSplice, + DM_Preproc_Number, + DM_Preproc_ParameterStringify, + DM_Preproc_Pragma, + DM_Preproc_Punctuator, + DM_Preproc_Punctuator_Colon, + DM_Preproc_Punctuator_Comma, + DM_Preproc_Punctuator_LeftBracket, + DM_Preproc_Punctuator_LeftParenthesis, + DM_Preproc_Punctuator_Period, + DM_Preproc_Punctuator_Question, + DM_Preproc_Punctuator_RightBracket, + DM_Preproc_Punctuator_RightParenthesis, + DM_Preproc_Punctuator_Semicolon, + DM_Preproc_StringBegin, + DM_Preproc_StringMiddle, + DM_Preproc_StringEnd, + DM_Preproc_TokenConcat, + DM_Preproc_Undefine, + DM_Preproc_Warning, + DM_Preproc_Whitespace, + + //DM + DM_And, + DM_AndAnd, + DM_AndEquals, + DM_AndAndEquals, + DM_As, + DM_AssignInto, + DM_Bar, + DM_BarBar, + DM_BarEquals, + DM_BarBarEquals, + DM_Break, + DM_Call, + DM_Catch, + DM_Colon, + DM_Comma, + DM_ConstantString, + DM_Continue, + DM_Dedent, + DM_Del, + DM_Do, + DM_DoubleSquareBracket, + DM_DoubleSquareBracketEquals, + DM_Else, + DM_Equals, + DM_EqualsEquals, + DM_Exclamation, + DM_ExclamationEquals, + DM_Float, + DM_For, + DM_Goto, + DM_GreaterThan, + DM_GreaterThanEquals, + DM_Identifier, + DM_If, + DM_In, + DM_Indent, + DM_IndeterminateArgs, + DM_RightShift, + DM_RightShiftEquals, + DM_Integer, + DM_LeftBracket, + DM_LeftCurlyBracket, + DM_LeftParenthesis, + DM_LeftShift, + DM_LeftShiftEquals, + DM_LessThan, + DM_LessThanEquals, + DM_Minus, + DM_MinusEquals, + DM_MinusMinus, + DM_Modulus, + DM_ModulusEquals, + DM_ModulusModulus, + DM_ModulusModulusEquals, + DM_New, + DM_Null, + DM_Period, + DM_Plus, + DM_PlusEquals, + DM_PlusPlus, + DM_Proc, + DM_Question, + DM_QuestionColon, + DM_QuestionLeftBracket, + DM_QuestionPeriod, + DM_RawString, + DM_Resource, + DM_Return, + DM_RightBracket, + DM_RightCurlyBracket, + DM_RightParenthesis, + DM_Semicolon, + DM_Set, + DM_Slash, + DM_SlashEquals, + DM_Spawn, + DM_Star, + DM_StarEquals, + DM_StarStar, + DM_Step, + DM_StringBegin, + DM_StringMiddle, + DM_StringEnd, + DM_SuperProc, + DM_Switch, + DM_Throw, + DM_Tilde, + DM_TildeEquals, + DM_TildeExclamation, + DM_To, + DM_Try, + DM_Var, + DM_While, + DM_Whitespace, + DM_Xor, + DM_XorEquals +} + +public struct Token(TokenType type, string text, Location location, object? value) { + public readonly TokenType Type = type; + public Location Location = location; + /// Use if you intend to show this to the user. + public readonly string Text = text; + public readonly object? Value = value; + + public string PrintableText => Text.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\t", "\\t"); + + public override string ToString() { + return $"{Type}({Location.ToString()}, {PrintableText})"; + } +} diff --git a/DMCompiler/DM/DMExpression.cs b/DMCompiler/DM/DMExpression.cs index 1a740669ba..ecc2f1971a 100644 --- a/DMCompiler/DM/DMExpression.cs +++ b/DMCompiler/DM/DMExpression.cs @@ -1,166 +1,162 @@ using DMCompiler.Bytecode; using DMCompiler.Compiler.DM; using DMCompiler.DM.Visitors; -using OpenDreamShared.Compiler; using System; using System.Diagnostics.CodeAnalysis; +using DMCompiler.Compiler; -namespace DMCompiler.DM { - internal abstract class DMExpression { - public Location Location; +namespace DMCompiler.DM; - protected DMExpression(Location location) { - Location = location; - } +internal abstract class DMExpression(Location location) { + public Location Location = location; - public static DMExpression Create(DMObject dmObject, DMProc proc, DMASTExpression expression, DreamPath? inferredPath = null) { - return DMExpressionBuilder.BuildExpression(expression, dmObject, proc, inferredPath); - } + public static DMExpression Create(DMObject dmObject, DMProc proc, DMASTExpression expression, DreamPath? inferredPath = null) { + return DMExpressionBuilder.BuildExpression(expression, dmObject, proc, inferredPath); + } - public static void Emit(DMObject dmObject, DMProc proc, DMASTExpression expression, DreamPath? inferredPath = null) { - var expr = Create(dmObject, proc, expression, inferredPath); - expr.EmitPushValue(dmObject, proc); - } + public static void Emit(DMObject dmObject, DMProc proc, DMASTExpression expression, DreamPath? inferredPath = null) { + var expr = Create(dmObject, proc, expression, inferredPath); + expr.EmitPushValue(dmObject, proc); + } - public static bool TryConstant(DMObject dmObject, DMProc proc, DMASTExpression expression, out Expressions.Constant? constant) { - var expr = Create(dmObject, proc, expression); - return expr.TryAsConstant(out constant); - } + public static bool TryConstant(DMObject dmObject, DMProc proc, DMASTExpression expression, out Expressions.Constant? constant) { + var expr = Create(dmObject, proc, expression); + return expr.TryAsConstant(out constant); + } - // Attempt to convert this expression into a Constant expression - public virtual bool TryAsConstant([NotNullWhen(true)] out Expressions.Constant? constant) { - constant = null; - return false; - } + // Attempt to convert this expression into a Constant expression + public virtual bool TryAsConstant([NotNullWhen(true)] out Expressions.Constant? constant) { + constant = null; + return false; + } - // Attempt to create a json-serializable version of this expression - public virtual bool TryAsJsonRepresentation(out object? json) { - json = null; - return false; - } + // Attempt to create a json-serializable version of this expression + public virtual bool TryAsJsonRepresentation(out object? json) { + json = null; + return false; + } - // Emits code that pushes the result of this expression to the proc's stack - // May throw if this expression is unable to be pushed to the stack - public abstract void EmitPushValue(DMObject dmObject, DMProc proc); + // Emits code that pushes the result of this expression to the proc's stack + // May throw if this expression is unable to be pushed to the stack + public abstract void EmitPushValue(DMObject dmObject, DMProc proc); - public enum ShortCircuitMode { - // If a dereference is short-circuited due to a null conditional, the short-circuit label should be jumped to with null NOT on top of the stack - PopNull, + public enum ShortCircuitMode { + // If a dereference is short-circuited due to a null conditional, the short-circuit label should be jumped to with null NOT on top of the stack + PopNull, - // If a dereference is short-circuited due to a null conditional, the short-circuit label should be jumped to with null still on the top of the stack - KeepNull, - } + // If a dereference is short-circuited due to a null conditional, the short-circuit label should be jumped to with null still on the top of the stack + KeepNull, + } - public virtual bool CanReferenceShortCircuit() => false; + public virtual bool CanReferenceShortCircuit() => false; - // Emits a reference that is to be used in an opcode that assigns/gets a value - // May throw if this expression is unable to be referenced - // The emitted code will jump to endLabel after pushing `null` to the stack in the event of a short-circuit - public virtual DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode = ShortCircuitMode.KeepNull) { - throw new CompileErrorException(Location, $"attempt to reference r-value"); - } + // Emits a reference that is to be used in an opcode that assigns/gets a value + // May throw if this expression is unable to be referenced + // The emitted code will jump to endLabel after pushing `null` to the stack in the event of a short-circuit + public virtual DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode = ShortCircuitMode.KeepNull) { + throw new CompileErrorException(Location, $"attempt to reference r-value"); + } - /// + /// /// Gets the canonical name of the expression if it exists. /// /// The name of the expression, or null if it does not have one. public virtual string? GetNameof(DMObject dmObject, DMProc proc) => null; - /// - /// Determines whether the expression returns an ambiguous path. - /// - /// Dereferencing these expressions will always skip validation via the "expr:y" operation. - public virtual bool PathIsFuzzy => false; + /// + /// Determines whether the expression returns an ambiguous path. + /// + /// Dereferencing these expressions will always skip validation via the "expr:y" operation. + public virtual bool PathIsFuzzy => false; - public virtual DreamPath? Path => null; + public virtual DreamPath? Path => null; - public virtual DreamPath? NestedPath => Path; - } - - // (a, b, c, ...) - // This isn't an expression, it's just a helper class for working with argument lists - sealed class ArgumentList { - public readonly (string? Name, DMExpression Expr)[] Expressions; - public int Length => Expressions.Length; - public Location Location; - - // Whether or not this has named arguments - private readonly bool _isKeyed; - - public ArgumentList(Location location, DMObject dmObject, DMProc proc, DMASTCallParameter[]? arguments, DreamPath? inferredPath = null) { - Location = location; - if (arguments == null) { - Expressions = Array.Empty<(string?, DMExpression)>(); - return; - } + public virtual DreamPath? NestedPath => Path; +} - Expressions = new (string?, DMExpression)[arguments.Length]; - - int idx = 0; - foreach(var arg in arguments) { - var value = DMExpression.Create(dmObject, proc, arg.Value, inferredPath); - var key = (arg.Key != null) ? DMExpression.Create(dmObject, proc, arg.Key, inferredPath) : null; - int argIndex = idx++; - string? name = null; - - switch (key) { - case Expressions.String keyStr: - name = keyStr.Value; - break; - case Expressions.Number keyNum: - //Replaces an ordered argument - argIndex = (int)keyNum.Value; - break; - case Expressions.Resource _: - case Expressions.Path _: - //The key becomes the value - value = key; - break; - - default: - if (key != null) { - DMCompiler.Emit(WarningCode.InvalidArgumentKey, key.Location, "Invalid argument key"); - } - - break; - } +// (a, b, c, ...) +// This isn't an expression, it's just a helper class for working with argument lists +sealed class ArgumentList { + public readonly (string? Name, DMExpression Expr)[] Expressions; + public int Length => Expressions.Length; + public Location Location; + + // Whether or not this has named arguments + private readonly bool _isKeyed; + + public ArgumentList(Location location, DMObject dmObject, DMProc proc, DMASTCallParameter[]? arguments, DreamPath? inferredPath = null) { + Location = location; + if (arguments == null) { + Expressions = Array.Empty<(string?, DMExpression)>(); + return; + } - if (name != null) - _isKeyed = true; + Expressions = new (string?, DMExpression)[arguments.Length]; + + int idx = 0; + foreach(var arg in arguments) { + var value = DMExpression.Create(dmObject, proc, arg.Value, inferredPath); + var key = (arg.Key != null) ? DMExpression.Create(dmObject, proc, arg.Key, inferredPath) : null; + int argIndex = idx++; + string? name = null; + + switch (key) { + case Expressions.String keyStr: + name = keyStr.Value; + break; + case Expressions.Number keyNum: + //Replaces an ordered argument + argIndex = (int)keyNum.Value; + break; + case Expressions.Resource _: + case Expressions.Path _: + //The key becomes the value + value = key; + break; + + default: + if (key != null) { + DMCompiler.Emit(WarningCode.InvalidArgumentKey, key.Location, "Invalid argument key"); + } - Expressions[argIndex] = (name, value); + break; } + + if (name != null) + _isKeyed = true; + + Expressions[argIndex] = (name, value); } + } - public (DMCallArgumentsType Type, int StackSize) EmitArguments(DMObject dmObject, DMProc proc) { - if (Expressions.Length == 0) { - return (DMCallArgumentsType.None, 0); - } + public (DMCallArgumentsType Type, int StackSize) EmitArguments(DMObject dmObject, DMProc proc) { + if (Expressions.Length == 0) { + return (DMCallArgumentsType.None, 0); + } - if (Expressions[0].Expr is Expressions.Arglist arglist) { - if (Expressions[0].Name != null) - DMCompiler.Emit(WarningCode.BadArgument, arglist.Location, "arglist cannot be a named argument"); + if (Expressions[0].Expr is Expressions.Arglist arglist) { + if (Expressions[0].Name != null) + DMCompiler.Emit(WarningCode.BadArgument, arglist.Location, "arglist cannot be a named argument"); - arglist.EmitPushArglist(dmObject, proc); - return (DMCallArgumentsType.FromArgumentList, 1); - } + arglist.EmitPushArglist(dmObject, proc); + return (DMCallArgumentsType.FromArgumentList, 1); + } - // TODO: Named arguments must come after all ordered arguments - int stackCount = 0; - foreach ((string name, DMExpression expr) in Expressions) { - if (_isKeyed) { - if (name != null) { - proc.PushString(name); - } else { - proc.PushNull(); - } + // TODO: Named arguments must come after all ordered arguments + int stackCount = 0; + foreach ((string name, DMExpression expr) in Expressions) { + if (_isKeyed) { + if (name != null) { + proc.PushString(name); + } else { + proc.PushNull(); } - - expr.EmitPushValue(dmObject, proc); - stackCount += _isKeyed ? 2 : 1; } - return (_isKeyed ? DMCallArgumentsType.FromStackKeyed : DMCallArgumentsType.FromStack, stackCount); + expr.EmitPushValue(dmObject, proc); + stackCount += _isKeyed ? 2 : 1; } + + return (_isKeyed ? DMCallArgumentsType.FromStackKeyed : DMCallArgumentsType.FromStack, stackCount); } } diff --git a/DMCompiler/DM/DMObject.cs b/DMCompiler/DM/DMObject.cs index de02e80c06..f77af86af4 100644 --- a/DMCompiler/DM/DMObject.cs +++ b/DMCompiler/DM/DMObject.cs @@ -1,207 +1,206 @@ -using OpenDreamShared.Dream; -using OpenDreamShared.Json; -using System; +using System; using System.Collections.Generic; using DMCompiler.Bytecode; -using OpenDreamShared.Compiler; +using DMCompiler.Compiler; +using DMCompiler.Json; + +namespace DMCompiler.DM; + +/// +/// This doesn't represent a particular, specific instance of an object,
+/// but rather stores the compile-time information necessary to describe a certain object definition,
+/// including its procs, vars, path, parent, etc. +///
+internal sealed class DMObject { + public int Id; + public DreamPath Path; + public DMObject? Parent; + public Dictionary> Procs = new(); + public Dictionary Variables = new(); + /// It's OK if the override var is not literally the exact same object as what it overrides. + public Dictionary VariableOverrides = new(); + public Dictionary GlobalVariables = new(); + /// A list of var and verb initializations implicitly done before the user's New() is called. + public HashSet ConstVariables = new(); + public HashSet TmpVariables = new(); + public List InitializationProcExpressions = new(); + public int? InitializationProc; + + public bool IsRoot => Path == DreamPath.Root; + + private List? _verbs; + + public DMObject(int id, DreamPath path, DMObject? parent) { + Id = id; + Path = path; + Parent = parent; + } -namespace DMCompiler.DM { - /// - /// This doesn't represent a particular, specific instance of an object,
- /// but rather stores the compile-time information necessary to describe a certain object definition,
- /// including its procs, vars, path, parent, etc. - ///
- internal sealed class DMObject { - public int Id; - public DreamPath Path; - public DMObject? Parent; - public Dictionary> Procs = new(); - public Dictionary Variables = new(); - /// It's OK if the override var is not literally the exact same object as what it overrides. - public Dictionary VariableOverrides = new(); - public Dictionary GlobalVariables = new(); - /// A list of var and verb initializations implicitly done before the user's New() is called. - public HashSet ConstVariables = new(); - public HashSet TmpVariables = new(); - public List InitializationProcExpressions = new(); - public int? InitializationProc; - - public bool IsRoot => Path == DreamPath.Root; - - private List? _verbs; - - public DMObject(int id, DreamPath path, DMObject? parent) { - Id = id; - Path = path; - Parent = parent; - } + public void AddProc(string name, DMProc proc) { + if (!Procs.ContainsKey(name)) Procs.Add(name, new List(1)); - public void AddProc(string name, DMProc proc) { - if (!Procs.ContainsKey(name)) Procs.Add(name, new List(1)); + Procs[name].Add(proc.Id); + } - Procs[name].Add(proc.Id); + /// + /// Note that this DOES NOT query our .
+ /// Make this (and other things) match the nomenclature of + ///
+ public DMVariable? GetVariable(string name) { + if (Variables.TryGetValue(name, out var variable)) { + return variable; } - /// - /// Note that this DOES NOT query our .
- /// Make this (and other things) match the nomenclature of - ///
- public DMVariable? GetVariable(string name) { - if (Variables.TryGetValue(name, out var variable)) { - return variable; - } + return Parent?.GetVariable(name); + } - return Parent?.GetVariable(name); - } + /// + /// Does a recursive search through self and parents to check if we already contain this variable, as a NON-STATIC VALUE! + /// + public bool HasLocalVariable(string name) { + if (Variables.ContainsKey(name)) + return true; + if (Parent == null) + return false; + return Parent.HasLocalVariable(name); + } - /// - /// Does a recursive search through self and parents to check if we already contain this variable, as a NON-STATIC VALUE! - /// - public bool HasLocalVariable(string name) { - if (Variables.ContainsKey(name)) - return true; - if (Parent == null) - return false; - return Parent.HasLocalVariable(name); - } + /// Similar to , just checks our globals/statics instead. + /// Does NOT return true if the global variable is in the root namespace, unless called on the Root object itself. + public bool HasGlobalVariable(string name) { + if (IsRoot) + return GlobalVariables.ContainsKey(name); + return HasGlobalVariableNotInRoot(name); + } - /// Similar to , just checks our globals/statics instead. - /// Does NOT return true if the global variable is in the root namespace, unless called on the Root object itself. - public bool HasGlobalVariable(string name) { - if (IsRoot) - return GlobalVariables.ContainsKey(name); - return HasGlobalVariableNotInRoot(name); - } + private bool HasGlobalVariableNotInRoot(string name) { + if (GlobalVariables.ContainsKey(name)) + return true; + if (Parent == null || Parent.IsRoot) + return false; + return Parent.HasGlobalVariable(name); + } - private bool HasGlobalVariableNotInRoot(string name) { - if (GlobalVariables.ContainsKey(name)) - return true; - if (Parent == null || Parent.IsRoot) - return false; - return Parent.HasGlobalVariable(name); - } + public bool HasProc(string name) { + if (Procs.ContainsKey(name)) return true; - public bool HasProc(string name) { - if (Procs.ContainsKey(name)) return true; + return Parent?.HasProc(name) ?? false; + } - return Parent?.HasProc(name) ?? false; - } + public bool HasProcNoInheritance(string name) { + return Procs.ContainsKey(name); + } - public bool HasProcNoInheritance(string name) { - return Procs.ContainsKey(name); - } + public List? GetProcs(string name) { + return Procs.GetValueOrDefault(name, Parent?.GetProcs(name) ?? null); + } - public List? GetProcs(string name) { - return Procs.GetValueOrDefault(name, Parent?.GetProcs(name) ?? null); - } + public void AddVerb(DMProc verb) { + _verbs ??= new(); + _verbs.Add(verb); + } - public void AddVerb(DMProc verb) { - _verbs ??= new(); - _verbs.Add(verb); - } + public DMVariable CreateGlobalVariable(DreamPath? type, string name, bool isConst, DMValueType valType = DMValueType.Anything) { + int id = DMObjectTree.CreateGlobal(out DMVariable global, type, name, isConst, valType); - public DMVariable CreateGlobalVariable(DreamPath? type, string name, bool isConst, DMValueType valType = DMValueType.Anything) { - int id = DMObjectTree.CreateGlobal(out DMVariable global, type, name, isConst, valType); + GlobalVariables[name] = id; + return global; + } - GlobalVariables[name] = id; - return global; + /// + /// Recursively searches for a global/static with the given name. + /// + /// Either the ID or null if no such global exists. + public int? GetGlobalVariableId(string name) { + if (GlobalVariables.TryGetValue(name, out int id)) { + return id; } - /// - /// Recursively searches for a global/static with the given name. - /// - /// Either the ID or null if no such global exists. - public int? GetGlobalVariableId(string name) { - if (GlobalVariables.TryGetValue(name, out int id)) { - return id; - } - - return Parent?.GetGlobalVariableId(name); - } + return Parent?.GetGlobalVariableId(name); + } - public DMVariable GetGlobalVariable(string name) { - int? id = GetGlobalVariableId(name); + public DMVariable GetGlobalVariable(string name) { + int? id = GetGlobalVariableId(name); - return (id == null) ? null : DMObjectTree.Globals[id.Value]; - } + return (id == null) ? null : DMObjectTree.Globals[id.Value]; + } - public void CreateInitializationProc() { - if (InitializationProcExpressions.Count > 0 && InitializationProc == null) { - var init = DMObjectTree.CreateDMProc(this, null); - InitializationProc = init.Id; - init.Call(DMReference.SuperProc, DMCallArgumentsType.None, 0); + public void CreateInitializationProc() { + if (InitializationProcExpressions.Count > 0 && InitializationProc == null) { + var init = DMObjectTree.CreateDMProc(this, null); + InitializationProc = init.Id; + init.Call(DMReference.SuperProc, DMCallArgumentsType.None, 0); - foreach (DMExpression expression in InitializationProcExpressions) { - try { - init.DebugSource(expression.Location); + foreach (DMExpression expression in InitializationProcExpressions) { + try { + init.DebugSource(expression.Location); - expression.EmitPushValue(this, init); - } catch (CompileErrorException e) { - DMCompiler.Emit(e.Error); - } + expression.EmitPushValue(this, init); + } catch (CompileErrorException e) { + DMCompiler.Emit(e.Error); } } } + } - public DreamTypeJson CreateJsonRepresentation() { - DreamTypeJson typeJson = new DreamTypeJson(); - - typeJson.Path = Path.PathString; - typeJson.Parent = Parent?.Id; + public DreamTypeJson CreateJsonRepresentation() { + DreamTypeJson typeJson = new DreamTypeJson { + Path = Path.PathString, + Parent = Parent?.Id + }; - if (Variables.Count > 0 || VariableOverrides.Count > 0) { - typeJson.Variables = new Dictionary(); + if (Variables.Count > 0 || VariableOverrides.Count > 0) { + typeJson.Variables = new Dictionary(); - foreach (KeyValuePair variable in Variables) { - if (!variable.Value.TryAsJsonRepresentation(out var valueJson)) - throw new Exception($"Failed to serialize {Path}.{variable.Key}"); + foreach (KeyValuePair variable in Variables) { + if (!variable.Value.TryAsJsonRepresentation(out var valueJson)) + throw new Exception($"Failed to serialize {Path}.{variable.Key}"); - typeJson.Variables.Add(variable.Key, valueJson); - } + typeJson.Variables.Add(variable.Key, valueJson); + } - foreach (KeyValuePair variable in VariableOverrides) { - if (!variable.Value.TryAsJsonRepresentation(out var valueJson)) - throw new Exception($"Failed to serialize {Path}.{variable.Key}"); + foreach (KeyValuePair variable in VariableOverrides) { + if (!variable.Value.TryAsJsonRepresentation(out var valueJson)) + throw new Exception($"Failed to serialize {Path}.{variable.Key}"); - typeJson.Variables[variable.Key] = valueJson; - } + typeJson.Variables[variable.Key] = valueJson; } + } - if (GlobalVariables.Count > 0) { - typeJson.GlobalVariables = GlobalVariables; - } + if (GlobalVariables.Count > 0) { + typeJson.GlobalVariables = GlobalVariables; + } - if (ConstVariables.Count > 0) { - typeJson.ConstVariables = ConstVariables; - } + if (ConstVariables.Count > 0) { + typeJson.ConstVariables = ConstVariables; + } - if (TmpVariables.Count > 0) { - typeJson.TmpVariables = TmpVariables; - } + if (TmpVariables.Count > 0) { + typeJson.TmpVariables = TmpVariables; + } - if (InitializationProc != null) { - typeJson.InitProc = InitializationProc; - } + if (InitializationProc != null) { + typeJson.InitProc = InitializationProc; + } - if (Procs.Count > 0) { - typeJson.Procs = new List>(Procs.Values); - } + if (Procs.Count > 0) { + typeJson.Procs = new List>(Procs.Values); + } - if (_verbs != null) { - typeJson.Verbs = new List(_verbs.Count); + if (_verbs != null) { + typeJson.Verbs = new List(_verbs.Count); - foreach (var verb in _verbs) { - typeJson.Verbs.Add(verb.Id); - } + foreach (var verb in _verbs) { + typeJson.Verbs.Add(verb.Id); } - - return typeJson; } - public bool IsSubtypeOf(DreamPath path) { - if (Path.IsDescendantOf(path)) return true; - if (Parent != null) return Parent.IsSubtypeOf(path); - return false; - } + return typeJson; + } + + public bool IsSubtypeOf(DreamPath path) { + if (Path.IsDescendantOf(path)) return true; + if (Parent != null) return Parent.IsSubtypeOf(path); + return false; } } diff --git a/DMCompiler/DM/DMObjectTree.cs b/DMCompiler/DM/DMObjectTree.cs index b601c02e58..f6942726d3 100644 --- a/DMCompiler/DM/DMObjectTree.cs +++ b/DMCompiler/DM/DMObjectTree.cs @@ -1,223 +1,226 @@ -using OpenDreamShared.Dream; -using OpenDreamShared.Json; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; +using DMCompiler.Compiler; using DMCompiler.Compiler.DM; -using JetBrains.Annotations; -using OpenDreamShared.Compiler; -using Robust.Shared.Utility; - -namespace DMCompiler.DM { - internal static class DMObjectTree { - public static readonly List AllObjects = new(); - public static readonly List AllProcs = new(); - - //TODO: These don't belong in the object tree - public static readonly List Globals = new(); - public static readonly Dictionary GlobalProcs = new(); - /// - /// Used to keep track of when we see a /proc/foo() or whatever, so that duplicates or missing definitions can be discovered, - /// even as GlobalProcs keeps clobbering old global proc overrides/definitions. - /// - public static readonly HashSet SeenGlobalProcDefinition = new(); - public static readonly List StringTable = new(); - public static DMProc GlobalInitProc; - public static readonly HashSet Resources = new(); - - public static DMObject Root => GetDMObject(DreamPath.Root)!; - - private static readonly Dictionary StringToStringId = new(); - private static readonly List<(int GlobalId, DMExpression Value)> _globalInitAssigns = new(); - - private static readonly Dictionary _pathToTypeId = new(); - private static int _dmObjectIdCounter; - private static int _dmProcIdCounter; - - static DMObjectTree() { - Reset(); - } +using DMCompiler.Json; - /// - /// A thousand curses upon you if you add a new member to this thing without deleting it here. - /// - public static void Reset() { - AllObjects.Clear(); - AllProcs.Clear(); - - Globals.Clear(); - GlobalProcs.Clear(); - SeenGlobalProcDefinition.Clear(); - StringTable.Clear(); - StringToStringId.Clear(); - Resources.Clear(); - - _globalInitAssigns.Clear(); - _pathToTypeId.Clear(); - _dmObjectIdCounter = 0; - _dmProcIdCounter = 0; - GlobalInitProc = new(-1, Root, null); - } +namespace DMCompiler.DM; - public static int AddString(string value) { - if (!StringToStringId.TryGetValue(value, out var stringId)) { - stringId = StringTable.Count; +internal static class DMObjectTree { + public static readonly List AllObjects = new(); + public static readonly List AllProcs = new(); - StringTable.Add(value); - StringToStringId.Add(value, stringId); - } + //TODO: These don't belong in the object tree + public static readonly List Globals = new(); + public static readonly Dictionary GlobalProcs = new(); + /// + /// Used to keep track of when we see a /proc/foo() or whatever, so that duplicates or missing definitions can be discovered, + /// even as GlobalProcs keeps clobbering old global proc overrides/definitions. + /// + public static readonly HashSet SeenGlobalProcDefinition = new(); + public static readonly List StringTable = new(); + public static DMProc GlobalInitProc; + public static readonly HashSet Resources = new(); - return stringId; - } + public static DMObject Root => GetDMObject(DreamPath.Root)!; + + private static readonly Dictionary StringToStringId = new(); + private static readonly List<(int GlobalId, DMExpression Value)> _globalInitAssigns = new(); + + private static readonly Dictionary _pathToTypeId = new(); + private static int _dmObjectIdCounter; + private static int _dmProcIdCounter; - public static DMProc CreateDMProc(DMObject dmObject, DMASTProcDefinition? astDefinition) { - DMProc dmProc = new DMProc(_dmProcIdCounter++, dmObject, astDefinition); - AllProcs.Add(dmProc); + static DMObjectTree() { + Reset(); + } + + /// + /// A thousand curses upon you if you add a new member to this thing without deleting it here. + /// + public static void Reset() { + AllObjects.Clear(); + AllProcs.Clear(); + + Globals.Clear(); + GlobalProcs.Clear(); + SeenGlobalProcDefinition.Clear(); + StringTable.Clear(); + StringToStringId.Clear(); + Resources.Clear(); + + _globalInitAssigns.Clear(); + _pathToTypeId.Clear(); + _dmObjectIdCounter = 0; + _dmProcIdCounter = 0; + GlobalInitProc = new(-1, Root, null); + } - return dmProc; + public static int AddString(string value) { + if (!StringToStringId.TryGetValue(value, out var stringId)) { + stringId = StringTable.Count; + + StringTable.Add(value); + StringToStringId.Add(value, stringId); } - [ContractAnnotation("createIfNonexistent:true => notnull")] - public static DMObject? GetDMObject(DreamPath path, bool createIfNonexistent = true) { - if (_pathToTypeId.TryGetValue(path, out int typeId)) { - return AllObjects[typeId]; - } - if (!createIfNonexistent) return null; - - DMObject? parent = null; - if (path.Elements.Length > 1) { - parent = GetDMObject(path.FromElements(0, -2)); // Create all parent classes as dummies, if we're being dummy-created too - } else if (path.Elements.Length == 1) { - switch (path.LastElement) { - case "client": - case "datum": - case "list": - case "savefile": - case "world": - parent = GetDMObject(DreamPath.Root); - break; - default: - parent = GetDMObject(DMCompiler.Settings.NoStandard ? DreamPath.Root : DreamPath.Datum); - break; - } - } + return stringId; + } + + public static DMProc CreateDMProc(DMObject dmObject, DMASTProcDefinition? astDefinition) { + DMProc dmProc = new DMProc(_dmProcIdCounter++, dmObject, astDefinition); + AllProcs.Add(dmProc); - DebugTools.Assert(path == DreamPath.Root || parent != null); // Parent SHOULD NOT be null here! (unless we're root lol) + return dmProc; + } - DMObject dmObject = new DMObject(_dmObjectIdCounter++, path, parent); - AllObjects.Add(dmObject); - _pathToTypeId[path] = dmObject.Id; - return dmObject; + public static DMObject? GetDMObject(DreamPath path, bool createIfNonexistent = true) { + if (_pathToTypeId.TryGetValue(path, out int typeId)) { + return AllObjects[typeId]; + } + if (!createIfNonexistent) return null; + + DMObject? parent = null; + if (path.Elements.Length > 1) { + parent = GetDMObject(path.FromElements(0, -2)); // Create all parent classes as dummies, if we're being dummy-created too + } else if (path.Elements.Length == 1) { + switch (path.LastElement) { + case "client": + case "datum": + case "list": + case "savefile": + case "world": + parent = GetDMObject(DreamPath.Root); + break; + default: + parent = GetDMObject(DMCompiler.Settings.NoStandard ? DreamPath.Root : DreamPath.Datum); + break; + } } - public static bool TryGetGlobalProc(string name, [NotNullWhen(true)] out DMProc? proc) { + if (path != DreamPath.Root && parent == null) // Parent SHOULD NOT be null here! (unless we're root lol) + throw new Exception($"Type {path} did not have a parent"); + + DMObject dmObject = new DMObject(_dmObjectIdCounter++, path, parent); + AllObjects.Add(dmObject); + _pathToTypeId[path] = dmObject.Id; + return dmObject; + } + + public static bool TryGetGlobalProc(string name, [NotNullWhen(true)] out DMProc? proc) { + if (!GlobalProcs.TryGetValue(name, out var id)) { proc = null; - return GlobalProcs.TryGetValue(name, out var id) && AllProcs.TryGetValue(id, out proc); + return false; } - /// True if the path exists, false if not. Keep in mind though that we may just have not found this object path yet while walking in ObjectBuilder. - public static bool TryGetTypeId(DreamPath path, out int typeId) { - return _pathToTypeId.TryGetValue(path, out typeId); - } + proc = AllProcs[id]; + return true; + } - // TODO: This is all so snowflake and needs redone - public static DreamPath? UpwardSearch(DreamPath path, DreamPath search) { - bool requireProcElement = search.Type == DreamPath.PathType.Absolute; - string? searchingProcName = null; - - int procElement = path.FindElement("proc"); - if (procElement == -1) procElement = path.FindElement("verb"); - if (procElement != -1) { - searchingProcName = search.LastElement; - path = path.RemoveElement(procElement); - search = search.FromElements(0, -2); - search.Type = DreamPath.PathType.Relative; - } + /// True if the path exists, false if not. Keep in mind though that we may just have not found this object path yet while walking in ObjectBuilder. + public static bool TryGetTypeId(DreamPath path, out int typeId) { + return _pathToTypeId.TryGetValue(path, out typeId); + } - procElement = search.FindElement("proc"); - if (procElement == -1) procElement = search.FindElement("verb"); - if (procElement != -1) { - searchingProcName = search.LastElement; - search = search.FromElements(0, procElement); - search.Type = DreamPath.PathType.Relative; - } + // TODO: This is all so snowflake and needs redone + public static DreamPath? UpwardSearch(DreamPath path, DreamPath search) { + bool requireProcElement = search.Type == DreamPath.PathType.Absolute; + string? searchingProcName = null; + + int procElement = path.FindElement("proc"); + if (procElement == -1) procElement = path.FindElement("verb"); + if (procElement != -1) { + searchingProcName = search.LastElement; + path = path.RemoveElement(procElement); + search = search.FromElements(0, -2); + search.Type = DreamPath.PathType.Relative; + } - if (searchingProcName == null && requireProcElement) - return null; + procElement = search.FindElement("proc"); + if (procElement == -1) procElement = search.FindElement("verb"); + if (procElement != -1) { + searchingProcName = search.LastElement; + search = search.FromElements(0, procElement); + search.Type = DreamPath.PathType.Relative; + } - DreamPath currentPath = path; - while (true) { - bool foundType = _pathToTypeId.TryGetValue(currentPath.Combine(search), out var foundTypeId); + if (searchingProcName == null && requireProcElement) + return null; - // We're searching for a proc - if (searchingProcName != null && foundType) { - DMObject type = AllObjects[foundTypeId]; + DreamPath currentPath = path; + while (true) { + bool foundType = _pathToTypeId.TryGetValue(currentPath.Combine(search), out var foundTypeId); - if (type.HasProc(searchingProcName)) { - return new DreamPath(type.Path.PathString + "/proc/" + searchingProcName); - } else if (foundTypeId == Root.Id && GlobalProcs.ContainsKey(searchingProcName)) { - return new DreamPath("/proc/" + searchingProcName); - } - } else if (foundType) { // We're searching for a type - return currentPath.Combine(search); - } + // We're searching for a proc + if (searchingProcName != null && foundType) { + DMObject type = AllObjects[foundTypeId]; - if (currentPath == DreamPath.Root) { - break; // Nothing found + if (type.HasProc(searchingProcName)) { + return new DreamPath(type.Path.PathString + "/proc/" + searchingProcName); + } else if (foundTypeId == Root.Id && GlobalProcs.ContainsKey(searchingProcName)) { + return new DreamPath("/proc/" + searchingProcName); } + } else if (foundType) { // We're searching for a type + return currentPath.Combine(search); + } - currentPath = currentPath.AddToPath(".."); + if (currentPath == DreamPath.Root) { + break; // Nothing found } - return null; + currentPath = currentPath.AddToPath(".."); } - public static int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, DMValueType valType = DMValueType.Anything) { - int id = Globals.Count; + return null; + } - global = new DMVariable(type, name, true, isConst, false, valType); - Globals.Add(global); - return id; - } + public static int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, DMValueType valType = DMValueType.Anything) { + int id = Globals.Count; - public static void AddGlobalProc(string name, int id) { - GlobalProcs[name] = id; // Said in this way so it clobbers previous definitions of this global proc (the ..() stuff doesn't work with glob procs) - } + global = new DMVariable(type, name, true, isConst, false, valType); + Globals.Add(global); + return id; + } - public static void AddGlobalInitAssign(int globalId, DMExpression value) { - _globalInitAssigns.Add( (globalId, value) ); - } + public static void AddGlobalProc(string name, int id) { + GlobalProcs[name] = id; // Said in this way so it clobbers previous definitions of this global proc (the ..() stuff doesn't work with glob procs) + } + + public static void AddGlobalInitAssign(int globalId, DMExpression value) { + _globalInitAssigns.Add( (globalId, value) ); + } - public static void CreateGlobalInitProc() { - if (_globalInitAssigns.Count == 0) return; + public static void CreateGlobalInitProc() { + if (_globalInitAssigns.Count == 0) return; - foreach (var assign in _globalInitAssigns) { - try { - GlobalInitProc.DebugSource(assign.Value.Location); + foreach (var assign in _globalInitAssigns) { + try { + GlobalInitProc.DebugSource(assign.Value.Location); - assign.Value.EmitPushValue(Root, GlobalInitProc); - GlobalInitProc.Assign(DMReference.CreateGlobal(assign.GlobalId)); - } catch (CompileErrorException e) { - DMCompiler.Emit(e.Error); - } + assign.Value.EmitPushValue(Root, GlobalInitProc); + GlobalInitProc.Assign(DMReference.CreateGlobal(assign.GlobalId)); + } catch (CompileErrorException e) { + DMCompiler.Emit(e.Error); } - - GlobalInitProc.ResolveLabels(); } - public static (DreamTypeJson[], ProcDefinitionJson[]) CreateJsonRepresentation() { - DreamTypeJson[] types = new DreamTypeJson[AllObjects.Count]; - ProcDefinitionJson[] procs = new ProcDefinitionJson[AllProcs.Count]; + GlobalInitProc.ResolveLabels(); + } - foreach (DMObject dmObject in AllObjects) { - types[dmObject.Id] = dmObject.CreateJsonRepresentation(); - } + public static (DreamTypeJson[], ProcDefinitionJson[]) CreateJsonRepresentation() { + DreamTypeJson[] types = new DreamTypeJson[AllObjects.Count]; + ProcDefinitionJson[] procs = new ProcDefinitionJson[AllProcs.Count]; - foreach (DMProc dmProc in AllProcs) { - procs[dmProc.Id] = dmProc.GetJsonRepresentation(); - } + foreach (DMObject dmObject in AllObjects) { + types[dmObject.Id] = dmObject.CreateJsonRepresentation(); + } - return (types, procs); + foreach (DMProc dmProc in AllProcs) { + procs[dmProc.Id] = dmProc.GetJsonRepresentation(); } + + return (types, procs); } } diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index fce59748f1..81b9922660 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -1,52 +1,32 @@ using DMCompiler.Bytecode; using DMCompiler.Compiler.DM; -using DMCompiler.DM.Visitors; -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; -using OpenDreamShared.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using DMCompiler.Compiler; +using DMCompiler.DM.Visitors; +using DMCompiler.Json; namespace DMCompiler.DM { internal sealed class DMProc { - public class LocalVariable { - public readonly string Name; - public readonly int Id; - public readonly bool IsParameter; - public DreamPath? Type; - - public LocalVariable(string name, int id, bool isParameter, DreamPath? type) { - Name = name; - Id = id; - IsParameter = isParameter; - Type = type; - } + public class LocalVariable(string name, int id, bool isParameter, DreamPath? type) { + public readonly string Name = name; + public readonly int Id = id; + public bool IsParameter = isParameter; + public DreamPath? Type = type; } - public sealed class LocalConstVariable : LocalVariable { - public readonly Expressions.Constant Value; - - public LocalConstVariable(string name, int id, DreamPath? type, Expressions.Constant value) - : base(name, id, false, type) { - Value = value; - } + public sealed class LocalConstVariable(string name, int id, DreamPath? type, Expressions.Constant value) + : LocalVariable(name, id, false, type) { + public readonly Expressions.Constant Value = value; } - private struct CodeLabelReference { - public readonly string Identifier; - public readonly string Placeholder; - public readonly Location Location; - public readonly DMProcScope Scope; - - public CodeLabelReference(string identifier, string placeholder, Location location, DMProcScope scope) { - Identifier = identifier; - Placeholder = placeholder; - Scope = scope; - Location = location; - } + private struct CodeLabelReference(string identifier, string placeholder, Location location, DMProcScope scope) { + public readonly string Identifier = identifier; + public readonly string Placeholder = placeholder; + public readonly Location Location = location; + public readonly DMProcScope Scope = scope; } public class CodeLabel { @@ -54,11 +34,11 @@ public class CodeLabel { public readonly string Name; public readonly long ByteOffset; - public int ReferencedCount = 0; + public int ReferencedCount; public string LabelName => $"{Name}_{Id}_codelabel"; - private static int _idCounter = 0; + private static int _idCounter ; public CodeLabel(string name, long offset) { Id = _idCounter++; @@ -254,7 +234,7 @@ public void ResolveCodeLabelReferences() { _labels.Add(reference.Placeholder, label.ByteOffset); label.ReferencedCount += 1; - // I was thinking about going through to replace all the placeholers + // I was thinking about going through to replace all the placeholders // with the actual label.LabelName, but it means I need to modify // _unresolvedLabels, being a list of tuple objects. Fuck that noise } @@ -295,12 +275,12 @@ public void ResolveLabels() { } private CodeLabel? GetCodeLabel(string name, DMProcScope? scope = null) { - DMProcScope? _scope = scope ?? _scopes.Peek(); - while (_scope != null) { - if (_scope.LocalCodeLabels.TryGetValue(name, out var localCodeLabel)) + scope ??= _scopes.Peek(); + while (scope != null) { + if (scope.LocalCodeLabels.TryGetValue(name, out var localCodeLabel)) return localCodeLabel; - _scope = _scope.ParentScope; + scope = scope.ParentScope; } return null; } diff --git a/OpenDreamShared/Dream/DMValueType.cs b/DMCompiler/DM/DMValueType.cs similarity index 81% rename from OpenDreamShared/Dream/DMValueType.cs rename to DMCompiler/DM/DMValueType.cs index b3ecfd178f..d86db71833 100644 --- a/OpenDreamShared/Dream/DMValueType.cs +++ b/DMCompiler/DM/DMValueType.cs @@ -1,6 +1,9 @@ using System; -namespace OpenDreamShared.Dream; +namespace DMCompiler.DM; + +// If you are modifying this, you must also modify OpenDreamShared.Dream.DreamValueType !! +// Unfortunately the client needs this and it can't reference DMCompiler due to the sandbox /// ///Stores any explicit casting done via the "as" keyword. Also stores compiler hints for DMStandard.
@@ -22,6 +25,7 @@ public enum DMValueType { CommandText = 0x400, Sound = 0x800, Icon = 0x1000, + //Byond here be dragons Unimplemented = 0x2000, // Marks that a method or property is not implemented. Throws a compiler warning if accessed. CompiletimeReadonly = 0x4000, // Marks that a property can only ever be read from, never written to. This is a const-ier version of const, for certain standard values like list.type diff --git a/DMCompiler/DM/DMVariable.cs b/DMCompiler/DM/DMVariable.cs index 6f2efaea20..9379a1afd8 100644 --- a/DMCompiler/DM/DMVariable.cs +++ b/DMCompiler/DM/DMVariable.cs @@ -1,47 +1,46 @@ using System.Diagnostics.CodeAnalysis; -using OpenDreamShared.Dream; -namespace DMCompiler.DM { - sealed class DMVariable { - public DreamPath? Type; - public string Name; - public bool IsGlobal; - /// - /// NOTE: This DMVariable may be forced constant through opendream_compiletimereadonly. This only marks that the variable has the DM quality of /const/ness. - /// - public bool IsConst; - public bool IsTmp; - public DMExpression Value; - public DMValueType ValType; +namespace DMCompiler.DM; - public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, bool isTmp, DMValueType valType = DMValueType.Anything) { - Type = type; - Name = name; - IsGlobal = isGlobal; - IsConst = isConst; - IsTmp = isTmp; - Value = null; - ValType = valType; - } +internal sealed class DMVariable { + public DreamPath? Type; + public string Name; + public bool IsGlobal; + /// + /// NOTE: This DMVariable may be forced constant through opendream_compiletimereadonly. This only marks that the variable has the DM quality of /const/ness. + /// + public bool IsConst; + public bool IsTmp; + public DMExpression Value; + public DMValueType ValType; - /// - /// This is a copy-on-write proc used to set the DMVariable to a constant value.
- /// In some contexts, doing so would clobber pre-existing constants,
- /// and so this sometimes creates a copy of , with the new constant value. - ///
- public DMVariable WriteToValue(Expressions.Constant value) { - if (Value == null) { - Value = value; - return this; - } + public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, bool isTmp, DMValueType valType = DMValueType.Anything) { + Type = type; + Name = name; + IsGlobal = isGlobal; + IsConst = isConst; + IsTmp = isTmp; + Value = null; + ValType = valType; + } - DMVariable clone = new DMVariable(Type, Name, IsGlobal, IsConst, IsTmp, ValType); - clone.Value = value; - return clone; + /// + /// This is a copy-on-write proc used to set the DMVariable to a constant value.
+ /// In some contexts, doing so would clobber pre-existing constants,
+ /// and so this sometimes creates a copy of , with the new constant value. + ///
+ public DMVariable WriteToValue(Expressions.Constant value) { + if (Value == null) { + Value = value; + return this; } - public bool TryAsJsonRepresentation([NotNullWhen(true)] out object? valueJson) { - return Value.TryAsJsonRepresentation(out valueJson); - } + DMVariable clone = new DMVariable(Type, Name, IsGlobal, IsConst, IsTmp, ValType); + clone.Value = value; + return clone; + } + + public bool TryAsJsonRepresentation([NotNullWhen(true)] out object? valueJson) { + return Value.TryAsJsonRepresentation(out valueJson); } } diff --git a/DMCompiler/DM/Expressions/Binary.cs b/DMCompiler/DM/Expressions/Binary.cs index 2853445b41..2cb274a447 100644 --- a/DMCompiler/DM/Expressions/Binary.cs +++ b/DMCompiler/DM/Expressions/Binary.cs @@ -1,16 +1,10 @@ using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; -using OpenDreamShared.Compiler; namespace DMCompiler.DM.Expressions { - abstract class BinaryOp : DMExpression { - protected DMExpression LHS { get; } - protected DMExpression RHS { get; } - - protected BinaryOp(Location location, DMExpression lhs, DMExpression rhs) : base(location) { - LHS = lhs; - RHS = rhs; - } + internal abstract class BinaryOp(Location location, DMExpression lhs, DMExpression rhs) : DMExpression(location) { + protected DMExpression LHS { get; } = lhs; + protected DMExpression RHS { get; } = rhs; } #region Simple @@ -487,10 +481,8 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) { #endregion #region Compound Assignment - abstract class AssignmentBinaryOp : BinaryOp { - public AssignmentBinaryOp(Location location, DMExpression lhs, DMExpression rhs) - : base(location, lhs, rhs) { } - + internal abstract class AssignmentBinaryOp(Location location, DMExpression lhs, DMExpression rhs) + : BinaryOp(location, lhs, rhs) { /// /// Generic interface for emitting the assignment operation. Has its conditionality and reference generation already handled. /// diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index 8705f03ce2..3a3a0abc5b 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -1,10 +1,9 @@ using DMCompiler.Bytecode; using DMCompiler.Compiler.DM; -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; -using OpenDreamShared.Json; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using DMCompiler.Compiler; +using DMCompiler.Json; namespace DMCompiler.DM.Expressions { // "abc[d]" diff --git a/DMCompiler/DM/Expressions/Constant.cs b/DMCompiler/DM/Expressions/Constant.cs index 598944ab41..44c49246c9 100644 --- a/DMCompiler/DM/Expressions/Constant.cs +++ b/DMCompiler/DM/Expressions/Constant.cs @@ -1,10 +1,10 @@ -using OpenDreamShared.Compiler; -using OpenDreamShared.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using DMCompiler.Compiler; +using DMCompiler.Json; namespace DMCompiler.DM.Expressions { abstract class Constant : DMExpression { diff --git a/DMCompiler/DM/Expressions/Dereference.cs b/DMCompiler/DM/Expressions/Dereference.cs index 4cd0375af6..c9e56c469f 100644 --- a/DMCompiler/DM/Expressions/Dereference.cs +++ b/DMCompiler/DM/Expressions/Dereference.cs @@ -1,9 +1,8 @@ using DMCompiler.Bytecode; -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using DMCompiler.Compiler; namespace DMCompiler.DM.Expressions { // x.y.z diff --git a/DMCompiler/DM/Expressions/LValue.cs b/DMCompiler/DM/Expressions/LValue.cs index 1ffc5c3e35..40c9b418ce 100644 --- a/DMCompiler/DM/Expressions/LValue.cs +++ b/DMCompiler/DM/Expressions/LValue.cs @@ -1,6 +1,6 @@ -using DMCompiler.Bytecode; -using OpenDreamShared.Compiler; using System.Diagnostics.CodeAnalysis; +using DMCompiler.Bytecode; +using DMCompiler.Compiler; namespace DMCompiler.DM.Expressions { abstract class LValue : DMExpression { diff --git a/DMCompiler/DM/Expressions/Procs.cs b/DMCompiler/DM/Expressions/Procs.cs index ca16cf3798..3a0a0716bc 100644 --- a/DMCompiler/DM/Expressions/Procs.cs +++ b/DMCompiler/DM/Expressions/Procs.cs @@ -1,9 +1,7 @@ -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; using System; using System.Linq; using DMCompiler.Bytecode; +using DMCompiler.Compiler; namespace DMCompiler.DM.Expressions { // x() (only the identifier) diff --git a/DMCompiler/DM/Expressions/Ternary.cs b/DMCompiler/DM/Expressions/Ternary.cs index 2ec880c57d..5f2c05b361 100644 --- a/DMCompiler/DM/Expressions/Ternary.cs +++ b/DMCompiler/DM/Expressions/Ternary.cs @@ -1,61 +1,44 @@ using System.Diagnostics.CodeAnalysis; -using OpenDreamShared.Compiler; -namespace DMCompiler.DM.Expressions { - // x ? y : z - sealed class Ternary : DMExpression { - private readonly DMExpression _a, _b, _c; +namespace DMCompiler.DM.Expressions; - public override bool PathIsFuzzy => true; +// x ? y : z +internal sealed class Ternary(Location location, DMExpression a, DMExpression b, DMExpression c) : DMExpression(location) { + public override bool PathIsFuzzy => true; - public Ternary(Location location, DMExpression a, DMExpression b, DMExpression c) : base(location) { - _a = a; - _b = b; - _c = c; + public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) { + if (!a.TryAsConstant(out var constant1)) { + constant = null; + return false; } - public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) { - if (!_a.TryAsConstant(out var a)) { - constant = null; - return false; - } - - if (a.IsTruthy()) { - return _b.TryAsConstant(out constant); - } - - return _c.TryAsConstant(out constant); + if (constant1.IsTruthy()) { + return b.TryAsConstant(out constant); } - public override void EmitPushValue(DMObject dmObject, DMProc proc) { - string cLabel = proc.NewLabelName(); - string endLabel = proc.NewLabelName(); - - _a.EmitPushValue(dmObject, proc); - proc.JumpIfFalse(cLabel); - _b.EmitPushValue(dmObject, proc); - proc.Jump(endLabel); - proc.AddLabel(cLabel); - _c.EmitPushValue(dmObject, proc); - proc.AddLabel(endLabel); - } + return c.TryAsConstant(out constant); } - // var in x to y - sealed class InRange : DMExpression { - private readonly DMExpression _var, _start, _end; - - public InRange(Location location, DMExpression var, DMExpression start, DMExpression end) : base(location) { - _var = var; - _start = start; - _end = end; - } + public override void EmitPushValue(DMObject dmObject, DMProc proc) { + string cLabel = proc.NewLabelName(); + string endLabel = proc.NewLabelName(); + + a.EmitPushValue(dmObject, proc); + proc.JumpIfFalse(cLabel); + b.EmitPushValue(dmObject, proc); + proc.Jump(endLabel); + proc.AddLabel(cLabel); + c.EmitPushValue(dmObject, proc); + proc.AddLabel(endLabel); + } +} - public override void EmitPushValue(DMObject dmObject, DMProc proc) { - _var.EmitPushValue(dmObject, proc); - _start.EmitPushValue(dmObject, proc); - _end.EmitPushValue(dmObject, proc); - proc.IsInRange(); - } +// var in x to y +internal sealed class InRange(Location location, DMExpression var, DMExpression start, DMExpression end) : DMExpression(location) { + public override void EmitPushValue(DMObject dmObject, DMProc proc) { + var.EmitPushValue(dmObject, proc); + start.EmitPushValue(dmObject, proc); + end.EmitPushValue(dmObject, proc); + proc.IsInRange(); } } diff --git a/DMCompiler/DM/Expressions/Unary.cs b/DMCompiler/DM/Expressions/Unary.cs index 59e4cb6c9a..98e691a7f3 100644 --- a/DMCompiler/DM/Expressions/Unary.cs +++ b/DMCompiler/DM/Expressions/Unary.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using DMCompiler.Bytecode; -using OpenDreamShared.Compiler; namespace DMCompiler.DM.Expressions { abstract class UnaryOp : DMExpression { diff --git a/OpenDreamShared/Dream/MatrixOpcodes.cs b/DMCompiler/DM/MatrixOpcodes.cs similarity index 91% rename from OpenDreamShared/Dream/MatrixOpcodes.cs rename to DMCompiler/DM/MatrixOpcodes.cs index 39e0b8637f..f9a95fde2d 100644 --- a/OpenDreamShared/Dream/MatrixOpcodes.cs +++ b/DMCompiler/DM/MatrixOpcodes.cs @@ -1,4 +1,4 @@ -namespace OpenDreamShared.Dream; +namespace DMCompiler.DM; /// /// These are the values associated with the wacky, undocumented /matrix() signatures.
diff --git a/OpenDreamShared/Dream/Procs/ProcAttributes.cs b/DMCompiler/DM/ProcAttributes.cs similarity index 78% rename from OpenDreamShared/Dream/Procs/ProcAttributes.cs rename to DMCompiler/DM/ProcAttributes.cs index 18435063d5..a6a9cdc98e 100644 --- a/OpenDreamShared/Dream/Procs/ProcAttributes.cs +++ b/DMCompiler/DM/ProcAttributes.cs @@ -1,10 +1,9 @@ using System; -namespace OpenDreamShared.Dream.Procs; +namespace DMCompiler.DM; [Flags] -public enum ProcAttributes -{ +public enum ProcAttributes { None = 1 << 0, // Internal IsOverride = 1 << 1, // Internal Unimplemented = 1 << 2, diff --git a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs index c50ccba9d5..9823271b65 100644 --- a/DMCompiler/DM/Visitors/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Visitors/DMExpressionBuilder.cs @@ -1,9 +1,7 @@ using DMCompiler.Compiler.DM; using DMCompiler.DM.Expressions; -using OpenDreamShared.Compiler; -using OpenDreamShared.Dream; -using Robust.Shared.Utility; using System; +using DMCompiler.Compiler; namespace DMCompiler.DM.Visitors; @@ -827,7 +825,6 @@ private static DMExpression BuildCall(DMASTCall call, DMObject dmObject, DMProc switch (call.CallParameters.Length) { default: DMCompiler.Emit(WarningCode.TooManyArguments, call.Location, "Too many arguments for call()"); - DebugTools.Assert(call.CallParameters.Length > 2); // This feels paranoid but, whatever goto case 2; // Fallthrough! case 2: { var a = DMExpression.Create(dmObject, proc, call.CallParameters[0].Value, inferredPath); diff --git a/DMCompiler/DM/Visitors/DMObjectBuilder.cs b/DMCompiler/DM/Visitors/DMObjectBuilder.cs index a86df00802..758346f232 100644 --- a/DMCompiler/DM/Visitors/DMObjectBuilder.cs +++ b/DMCompiler/DM/Visitors/DMObjectBuilder.cs @@ -1,7 +1,6 @@ -using OpenDreamShared.Compiler; using DMCompiler.Compiler.DM; -using OpenDreamShared.Dream; using System.Collections.Generic; +using DMCompiler.Compiler; namespace DMCompiler.DM.Visitors { internal static class DMObjectBuilder { diff --git a/DMCompiler/DM/Visitors/DMProcBuilder.cs b/DMCompiler/DM/Visitors/DMProcBuilder.cs index 14e540c08f..83d232faa6 100644 --- a/DMCompiler/DM/Visitors/DMProcBuilder.cs +++ b/DMCompiler/DM/Visitors/DMProcBuilder.cs @@ -1,15 +1,13 @@ -using OpenDreamShared.Compiler; using DMCompiler.Compiler.DM; using System.Collections.Generic; -using OpenDreamShared.Dream; using System; using DMCompiler.DM.Expressions; -using OpenDreamShared.Dream.Procs; using System.Diagnostics; using DMCompiler.Bytecode; +using DMCompiler.Compiler; namespace DMCompiler.DM.Visitors { - sealed class DMProcBuilder { + internal sealed class DMProcBuilder { private readonly DMObject _dmObject; private readonly DMProc _proc; diff --git a/DMCompiler/DMCompiler.cs b/DMCompiler/DMCompiler.cs index ab396e178a..1f5af12f48 100644 --- a/DMCompiler/DMCompiler.cs +++ b/DMCompiler/DMCompiler.cs @@ -4,9 +4,6 @@ using DMCompiler.Compiler.DMPreprocessor; using DMCompiler.DM; using DMCompiler.DM.Visitors; -using OpenDreamShared.Compiler; -using OpenDreamShared.Json; -using Robust.Shared.Utility; using System; using System.Collections.Generic; using System.Globalization; @@ -16,6 +13,8 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using DMCompiler.Compiler; +using DMCompiler.Json; namespace DMCompiler; @@ -351,8 +350,9 @@ public static void SetPragma(WarningCode code, ErrorLevel level) { } public static ErrorLevel CodeToLevel(WarningCode code) { - bool didFind = Config.ErrorConfig.TryGetValue(code, out var ret); - DebugTools.Assert(didFind); + if (!Config.ErrorConfig.TryGetValue(code, out var ret)) + throw new Exception($"Failed to find error level for code {code}"); + return ret; } } diff --git a/DMCompiler/DMCompiler.csproj b/DMCompiler/DMCompiler.csproj index 82cddd4ad9..77d24af309 100644 --- a/DMCompiler/DMCompiler.csproj +++ b/DMCompiler/DMCompiler.csproj @@ -13,10 +13,6 @@ - - - - diff --git a/OpenDreamShared/Json/DreamCompiledJson.cs b/DMCompiler/Json/DreamCompiledJson.cs similarity index 90% rename from OpenDreamShared/Json/DreamCompiledJson.cs rename to DMCompiler/Json/DreamCompiledJson.cs index a6a7744542..aa52136559 100644 --- a/OpenDreamShared/Json/DreamCompiledJson.cs +++ b/DMCompiler/Json/DreamCompiledJson.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace OpenDreamShared.Json; +namespace DMCompiler.Json; public sealed class DreamCompiledJson { public DreamCompiledJsonMetadata Metadata { get; set; } @@ -19,5 +19,5 @@ public sealed class DreamCompiledJsonMetadata { /// /// Hash of all the DreamProcOpcodes /// - public string Version { get; set; } + public required string Version { get; set; } } diff --git a/DMCompiler/Json/DreamMapJson.cs b/DMCompiler/Json/DreamMapJson.cs new file mode 100644 index 0000000000..d04cbcf03a --- /dev/null +++ b/DMCompiler/Json/DreamMapJson.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace DMCompiler.Json; + +public sealed class DreamMapJson { + public int MaxX { get; set; } + public int MaxY { get; set; } + public int MaxZ { get; set; } + public Dictionary CellDefinitions { get; set; } = new(); + public List Blocks { get; set; } = new(); +} + +public sealed class CellDefinitionJson { + public string Name { get; set; } + public MapObjectJson Turf { get; set; } + public MapObjectJson Area { get; set; } + public List Objects { get; set; } = new(); + + public CellDefinitionJson(string name) { + Name = name; + } +} + +public sealed class MapObjectJson(int type) { + public int Type { get; set; } = type; + public Dictionary? VarOverrides { get; set; } + + public bool AddVarOverride(string varName, object varValue) { + VarOverrides ??= new(); + bool contained = VarOverrides.ContainsKey(varName); + VarOverrides[varName] = varValue; + return !contained; + } + + public override bool Equals(object? obj) { + return obj is MapObjectJson json && + Type == json.Type && + EqualityComparer>.Default.Equals(VarOverrides, json.VarOverrides); + } + + public override int GetHashCode() { + return HashCode.Combine(Type, VarOverrides); + } +} + +public sealed class MapBlockJson(int x, int y, int z) { + public int X { get; set; } = x; + public int Y { get; set; } = y; + public int Z { get; set; } = z; + public int Width { get; set; } + public int Height { get; set; } + public List Cells { get; set; } = new(); +} diff --git a/DMCompiler/Json/DreamObjectJson.cs b/DMCompiler/Json/DreamObjectJson.cs new file mode 100644 index 0000000000..fe41a2bd45 --- /dev/null +++ b/DMCompiler/Json/DreamObjectJson.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace DMCompiler.Json; + +public enum JsonVariableType { + Resource = 0, + Type = 1, + Proc = 2, + List = 3, + PositiveInfinity = 4, + NegativeInfinity = 5 +} + +public sealed class DreamTypeJson { + public string Path { get; set; } + public int? Parent { get; set; } + public int? InitProc { get; set; } + public List> Procs { get; set; } + public List Verbs { get; set; } + public Dictionary Variables { get; set; } + public Dictionary GlobalVariables { get; set; } + public HashSet? ConstVariables { get; set; } + public HashSet? TmpVariables { get; set; } +} + +public sealed class GlobalListJson { + public int GlobalCount { get; set; } + public List Names { get; set; } + public Dictionary Globals { get; set; } +} diff --git a/DMCompiler/Json/DreamProcJson.cs b/DMCompiler/Json/DreamProcJson.cs new file mode 100644 index 0000000000..5c26be6da2 --- /dev/null +++ b/DMCompiler/Json/DreamProcJson.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using DMCompiler.DM; + +namespace DMCompiler.Json; + +public sealed class ProcDefinitionJson { + public int OwningTypeId { get; set; } + public string Name { get; set; } + public bool IsVerb { get; set; } + public int MaxStackSize { get; set; } + public List? Arguments { get; set; } + public List Locals { get; set; } + public ProcAttributes Attributes { get; set; } = ProcAttributes.None; + public List SourceInfo { get; set; } + public byte[]? Bytecode { get; set; } + + public string? VerbName { get; set; } + public string? VerbCategory { get; set; } = null; + public string? VerbDesc { get; set; } + public sbyte Invisibility { get; set; } +} + +public sealed class ProcArgumentJson { + public string Name { get; set; } + public DMValueType Type { get; set; } +} + +public sealed class LocalVariableJson { + public int Offset { get; set; } + public int? Remove { get; set; } + public string Add { get; set; } +} + +public sealed class SourceInfoJson { + public int Offset { get; set; } + public int? File { get; set; } + public int Line { get; set; } +} diff --git a/DMCompiler/Location.cs b/DMCompiler/Location.cs new file mode 100644 index 0000000000..e4895b167a --- /dev/null +++ b/DMCompiler/Location.cs @@ -0,0 +1,33 @@ +using System.Text; + +namespace DMCompiler; + +public readonly struct Location(string filePath, int? line, int? column) { + /// + /// For when DM location information can't be determined. + /// + public static readonly Location Unknown = new(); + + /// + /// For when internal OpenDream warnings/errors are raised or something internal needs to be passed a location. + /// + public static readonly Location Internal = new("", null, null); + + public string SourceFile { get; } = filePath; + public int? Line { get; } = line; + public int? Column { get; } = column; + + public override string ToString() { + var builder = new StringBuilder(SourceFile ?? ""); + + if (Line is not null) { + builder.Append(":" + Line); + + if (Column is not null) { + builder.Append(":" + Column); + } + } + + return builder.ToString(); + } +} diff --git a/DMCompiler/Program.cs b/DMCompiler/Program.cs index 4c89e178f5..0eb89396e1 100644 --- a/DMCompiler/Program.cs +++ b/DMCompiler/Program.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Robust.Shared.Utility; namespace DMCompiler; @@ -100,7 +99,6 @@ private static bool TryParseArguments(string[] args, out DMCompilerSettings sett Console.WriteLine("Compiler arg 'define' requires macro identifier for definition directive"); return false; } - DebugTools.Assert(parts is { Length: <= 2 }); settings.MacroDefines ??= new Dictionary(); settings.MacroDefines[parts[0]] = parts.Length > 1 ? parts[1] : ""; break; diff --git a/DMDisassembler/DMProc.cs b/DMDisassembler/DMProc.cs index 4cea3718e4..f692debd09 100644 --- a/DMDisassembler/DMProc.cs +++ b/DMDisassembler/DMProc.cs @@ -1,65 +1,55 @@ using OpenDreamRuntime.Procs; -using OpenDreamShared.Json; using System; using System.Collections.Generic; using System.Text; +using DMCompiler.Json; -namespace DMDisassembler { - class DMProc { - private class DecompiledOpcode { - public int Position; - public string Text; +namespace DMDisassembler; - public DecompiledOpcode(int position, string text) { - Position = position; - Text = text; - } - } +internal class DMProc(ProcDefinitionJson json) { + private class DecompiledOpcode(int position, string text) { + public readonly int Position = position; + public readonly string Text = text; + } - public string Name; - public byte[] Bytecode; - public Exception exception; + public string Name = json.Name; + public byte[] Bytecode = json.Bytecode ?? Array.Empty(); + public Exception exception; - public DMProc(ProcDefinitionJson json) { - Name = json.Name; - Bytecode = json.Bytecode ?? Array.Empty(); - } + public string Decompile() { + List decompiled = new(); + HashSet labeledPositions = new(); - public string Decompile() { - List decompiled = new(); - HashSet labeledPositions = new(); - - try { - foreach (var (position, instruction) in new ProcDecoder(Program.CompiledJson.Strings, Bytecode).Disassemble()) { - decompiled.Add(new DecompiledOpcode(position, ProcDecoder.Format(instruction, type => Program.CompiledJson.Types[type].Path))); - if (ProcDecoder.GetJumpDestination(instruction) is int jumpPosition) { - labeledPositions.Add(jumpPosition); - } + try { + foreach (var (position, instruction) in new ProcDecoder(Program.CompiledJson.Strings, Bytecode).Disassemble()) { + decompiled.Add(new DecompiledOpcode(position, ProcDecoder.Format(instruction, type => Program.CompiledJson.Types[type].Path))); + if (ProcDecoder.GetJumpDestination(instruction) is int jumpPosition) { + labeledPositions.Add(jumpPosition); } - } catch (Exception ex) { - exception = ex; - } - - StringBuilder result = new StringBuilder(); - foreach (DecompiledOpcode decompiledOpcode in decompiled) { - if (labeledPositions.Contains(decompiledOpcode.Position)) { - result.AppendFormat("0x{0:x}", decompiledOpcode.Position); - } - result.Append('\t'); - result.AppendLine(decompiledOpcode.Text); } + } catch (Exception ex) { + exception = ex; + } - if (labeledPositions.Contains(Bytecode.Length)) { - // In case of a Jump off the end of the proc. - result.AppendFormat("0x{0:x}", Bytecode.Length); - result.AppendLine(); + StringBuilder result = new StringBuilder(); + foreach (DecompiledOpcode decompiledOpcode in decompiled) { + if (labeledPositions.Contains(decompiledOpcode.Position)) { + result.AppendFormat("0x{0:x}", decompiledOpcode.Position); } + result.Append('\t'); + result.AppendLine(decompiledOpcode.Text); + } - if (exception != null) { - result.Append(exception); - } + if (labeledPositions.Contains(Bytecode.Length)) { + // In case of a Jump off the end of the proc. + result.AppendFormat("0x{0:x}", Bytecode.Length); + result.AppendLine(); + } - return result.ToString(); + if (exception != null) { + result.Append(exception); } + + return result.ToString(); } } diff --git a/DMDisassembler/DMType.cs b/DMDisassembler/DMType.cs index 6d200a4412..d7a9dd56f4 100644 --- a/DMDisassembler/DMType.cs +++ b/DMDisassembler/DMType.cs @@ -1,5 +1,5 @@ -using OpenDreamShared.Json; -using System.Collections.Generic; +using System.Collections.Generic; +using DMCompiler.Json; namespace DMDisassembler; diff --git a/DMDisassembler/Program.cs b/DMDisassembler/Program.cs index 92b266eed8..c7a33e3181 100644 --- a/DMDisassembler/Program.cs +++ b/DMDisassembler/Program.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; -using OpenDreamShared.Json; +using DMCompiler.Json; namespace DMDisassembler; diff --git a/OpenDreamClient/Interface/DMF/DMFLexer.cs b/OpenDreamClient/Interface/DMF/DMFLexer.cs index 1eed5065a3..0a28d61bea 100644 --- a/OpenDreamClient/Interface/DMF/DMFLexer.cs +++ b/OpenDreamClient/Interface/DMF/DMFLexer.cs @@ -1,109 +1,131 @@ using System.Text; -using OpenDreamShared.Compiler; namespace OpenDreamClient.Interface.DMF; -public sealed class DMFLexer : TextLexer { +public sealed class DMFLexer(string source) { + public enum TokenType { + Error, + + EndOfFile, + Newline, + Period, + Semicolon, + Equals, + Value, + Elem, + Macro, + Menu, + Window, + Attribute + } + + public struct Token(TokenType type, string text) { + public TokenType Type = type; + public string Text = text; + + public Token(TokenType type, char textChar) : this(type, textChar.ToString()) { } + } + + private int _currentSourceIndex; + /// /// Whether we're parsing an attribute name or attribute value /// private bool _parsingAttributeName = true; - public DMFLexer(string sourceName, string source) : base(sourceName, source) { } - - protected override Token ParseNextToken() { - Token token = base.ParseNextToken(); - - if (token.Type == TokenType.Unknown) { - char c = GetCurrent(); + private bool AtEndOfSource => _currentSourceIndex >= source.Length; + + public Token NextToken() { + char c = GetCurrent(); + + while (c is ' ' or '\r' or '\t') // Skip these + c = Advance(); + + switch (c) { + case '\0': + return new(TokenType.EndOfFile, c); + case '\n': + Advance(); + _parsingAttributeName = true; + return new(TokenType.Newline, c); + case '.': + Advance(); + _parsingAttributeName = true; // Still parsing an attribute name, the last one was actually an element name! + return new(TokenType.Period, c); + case ';': + Advance(); + _parsingAttributeName = true; + return new(TokenType.Semicolon, c); + case '=': + Advance(); + _parsingAttributeName = false; + return new(TokenType.Equals, c); + case '\'': // TODO: Single-quoted values probably refer to resources and shouldn't be treated as strings + case '"': { + StringBuilder textBuilder = new StringBuilder(c.ToString()); + + while (Advance() != c && !AtEndOfSource) { + if (GetCurrent() == '\\') { + Advance(); - switch (c) { - case ' ': - case '\r': - case '\t': { - Advance(); - token = CreateToken(TokenType.Skip, c); - break; - } - case '.': - Advance(); - token = CreateToken(TokenType.DMF_Period, c); - _parsingAttributeName = true; // Still parsing an attribute name, the last one was actually an element name! - break; - case ';': - Advance(); - token = CreateToken(TokenType.DMF_Semicolon, c); - _parsingAttributeName = true; - break; - case '=': - Advance(); - token = CreateToken(TokenType.DMF_Equals, c); - _parsingAttributeName = false; - break; - case '\'': // TODO: Single-quoted values probably refer to resources and shouldn't be treated as strings - case '"': { - StringBuilder textBuilder = new StringBuilder(c.ToString()); - - while (Advance() != c && !AtEndOfSource) { - if (GetCurrent() == '\\') { - Advance(); - - switch (GetCurrent()) { - case '"': - case '\\': textBuilder.Append(GetCurrent()); break; - case 't': textBuilder.Append('\t'); break; - case 'n': textBuilder.Append('\n'); break; - default: throw new Exception($"Invalid escape sequence '\\{GetCurrent()}'"); - } - } else { - textBuilder.Append(GetCurrent()); + switch (GetCurrent()) { + case '"': + case '\\': textBuilder.Append(GetCurrent()); break; + case 't': textBuilder.Append('\t'); break; + case 'n': textBuilder.Append('\n'); break; + default: throw new Exception($"Invalid escape sequence '\\{GetCurrent()}'"); } + } else { + textBuilder.Append(GetCurrent()); } - if (GetCurrent() != c) throw new Exception($"Expected '{c}'"); - textBuilder.Append(c); - Advance(); + } + if (GetCurrent() != c) throw new Exception($"Expected '{c}'"); + textBuilder.Append(c); + Advance(); - string text = textBuilder.ToString(); + string text = textBuilder.ToString(); - // Strings are treated the same un-quoted values except they can use escape codes - token = CreateToken(TokenType.DMF_Value, text.Substring(1, text.Length - 2)); - break; + // Strings are treated the same un-quoted values except they can use escape codes + return new(TokenType.Value, text.Substring(1, text.Length - 2)); + } + default: { + if (!char.IsAscii(c)) { + Advance(); + return new(TokenType.Error, $"Invalid character: {c.ToString()}"); } - default: { - if (!char.IsAscii(c)) { - token = CreateToken(TokenType.Error, $"Invalid character: {c.ToString()}"); - Advance(); - break; - } - - string text = c.ToString(); - while (!char.IsWhiteSpace(Advance()) && GetCurrent() is not ';' and not '=' and not '.' && !AtEndOfSource) text += GetCurrent(); + string text = c.ToString(); - TokenType tokenType; - if (_parsingAttributeName) { - tokenType = text switch { - "elem" => TokenType.DMF_Elem, - "macro" => TokenType.DMF_Macro, - "menu" => TokenType.DMF_Menu, - "window" => TokenType.DMF_Window, - _ => TokenType.DMF_Attribute - }; + while (!char.IsWhiteSpace(Advance()) && GetCurrent() is not ';' and not '=' and not '.' && !AtEndOfSource) + text += GetCurrent(); - _parsingAttributeName = false; - } else { - tokenType = TokenType.DMF_Value; - _parsingAttributeName = true; - } + TokenType tokenType; + if (_parsingAttributeName) { + tokenType = text switch { + "elem" => TokenType.Elem, + "macro" => TokenType.Macro, + "menu" => TokenType.Menu, + "window" => TokenType.Window, + _ => TokenType.Attribute + }; - token = CreateToken(tokenType, text); - break; + _parsingAttributeName = false; + } else { + tokenType = TokenType.Value; + _parsingAttributeName = true; } + + return new(tokenType, text); } - } else if (token.Type == TokenType.Newline) { - _parsingAttributeName = true; } + } + + private char GetCurrent() { + return !AtEndOfSource ? source[_currentSourceIndex] : '\0'; + } - return token; + private char Advance() { + _currentSourceIndex++; + return GetCurrent(); } } diff --git a/OpenDreamClient/Interface/DMF/DMFParser.cs b/OpenDreamClient/Interface/DMF/DMFParser.cs index 397edc8b53..fdc931caab 100644 --- a/OpenDreamClient/Interface/DMF/DMFParser.cs +++ b/OpenDreamClient/Interface/DMF/DMFParser.cs @@ -1,23 +1,25 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using OpenDreamClient.Interface.Descriptors; -using OpenDreamShared.Compiler; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown.Mapping; +using Token = OpenDreamClient.Interface.DMF.DMFLexer.Token; +using TokenType = OpenDreamClient.Interface.DMF.DMFLexer.TokenType; namespace OpenDreamClient.Interface.DMF; -public sealed class DMFParser : Parser { - private readonly ISerializationManager _serializationManager; +public sealed class DMFParser(DMFLexer lexer, ISerializationManager serializationManager) { + public List Errors = new(); private readonly TokenType[] _attributeTokenTypes = { - TokenType.DMF_Attribute, - TokenType.DMF_Macro, - TokenType.DMF_Menu + TokenType.Attribute, + TokenType.Macro, + TokenType.Menu }; - public DMFParser(DMFLexer lexer, ISerializationManager serializationManager) : base(lexer) { - _serializationManager = serializationManager; - } + private Token _currentToken = lexer.NextToken(); + private bool _errorMode; + private readonly Queue _tokenQueue = new(); /// /// Parse the command used in a global winset() @@ -39,32 +41,32 @@ public InterfaceDescriptor Interface() { bool parsing = true; while (parsing) { - try { - WindowDescriptor? windowDescriptor = Window(); - if (windowDescriptor != null) { - windowDescriptors.Add(windowDescriptor); - Newline(); - } + WindowDescriptor? windowDescriptor = Window(); + if (windowDescriptor != null) { + windowDescriptors.Add(windowDescriptor); + Newline(); + } - MacroSetDescriptor? macroSet = MacroSet(); - if (macroSet != null) { - macroSetDescriptors.Add(macroSet); - Newline(); - } + MacroSetDescriptor? macroSet = MacroSet(); + if (macroSet != null) { + macroSetDescriptors.Add(macroSet); + Newline(); + } - MenuDescriptor? menu = Menu(); - if (menu != null) { - menuDescriptors.Add(menu); - Newline(); - } + MenuDescriptor? menu = Menu(); + if (menu != null) { + menuDescriptors.Add(menu); + Newline(); + } - if (windowDescriptor == null && macroSet == null && menu == null) { - parsing = false; - } - } catch (CompileErrorException) { + if (windowDescriptor == null && macroSet == null && menu == null) { + parsing = false; + } + + if (_errorMode) { //Error recovery Token token = Current(); - while (token.Type is not (TokenType.DMF_Window or TokenType.DMF_Macro or TokenType.DMF_Menu)) { + while (token.Type is not (TokenType.Window or TokenType.Macro or TokenType.Menu)) { token = Advance(); if (token.Type == TokenType.EndOfFile) { @@ -80,9 +82,9 @@ public InterfaceDescriptor Interface() { } private WindowDescriptor? Window() { - if (Check(TokenType.DMF_Window)) { + if (Check(TokenType.Window)) { Token windowIdToken = Current(); - Consume(TokenType.DMF_Value, "Expected a window id"); + Consume(TokenType.Value, "Expected a window id"); string windowId = windowIdToken.Text; Newline(); @@ -96,18 +98,20 @@ public InterfaceDescriptor Interface() { } private bool Element(WindowDescriptor window) { - if (Check(TokenType.DMF_Elem)) { + if (Check(TokenType.Elem)) { Token elementIdToken = Current(); - Consume(TokenType.DMF_Value, "Expected an element id"); + Consume(TokenType.Value, "Expected an element id"); string elementId = elementIdToken.Text; Newline(); var attributes = Attributes(); attributes.Add("id", elementId); - var control = window.CreateChildDescriptor(_serializationManager, attributes); - if (control == null) + var control = window.CreateChildDescriptor(serializationManager, attributes); + if (control == null) { Error($"Element '{elementId}' does not have a valid 'type' attribute"); + return false; + } return true; } @@ -116,9 +120,9 @@ private bool Element(WindowDescriptor window) { } private MacroSetDescriptor? MacroSet() { - if (Check(TokenType.DMF_Macro)) { + if (Check(TokenType.Macro)) { Token macroSetIdToken = Current(); - Consume(TokenType.DMF_Value, "Expected a macro set id"); + Consume(TokenType.Value, "Expected a macro set id"); Newline(); MacroSetDescriptor macroSet = new(macroSetIdToken.Text); @@ -131,9 +135,9 @@ private bool Element(WindowDescriptor window) { } private bool Macro(MacroSetDescriptor macroSet) { - if (Check(TokenType.DMF_Elem)) { + if (Check(TokenType.Elem)) { Token macroIdToken = Current(); - bool hasId = Check(TokenType.DMF_Value); + bool hasId = Check(TokenType.Value); Newline(); var attributes = Attributes(); @@ -141,7 +145,7 @@ private bool Macro(MacroSetDescriptor macroSet) { if (hasId) attributes.Add("id", macroIdToken.Text); else attributes.Add("id", attributes.Get("name")); - macroSet.CreateChildDescriptor(_serializationManager, attributes); + macroSet.CreateChildDescriptor(serializationManager, attributes); return true; } @@ -149,9 +153,9 @@ private bool Macro(MacroSetDescriptor macroSet) { } private MenuDescriptor? Menu() { - if (Check(TokenType.DMF_Menu)) { + if (Check(TokenType.Menu)) { Token menuIdToken = Current(); - Consume(TokenType.DMF_Value, "Expected a menu id"); + Consume(TokenType.Value, "Expected a menu id"); Newline(); var menu = new MenuDescriptor(menuIdToken.Text); @@ -164,9 +168,9 @@ private bool Macro(MacroSetDescriptor macroSet) { } private bool MenuElement(MenuDescriptor menu) { - if (Check(TokenType.DMF_Elem)) { + if (Check(TokenType.Elem)) { Token elementIdToken = Current(); - bool hasId = Check(TokenType.DMF_Value); + bool hasId = Check(TokenType.Value); Newline(); var attributes = Attributes(); @@ -174,7 +178,7 @@ private bool MenuElement(MenuDescriptor menu) { if (hasId) attributes.Add("id", elementIdToken.Text); else attributes.Add("id", attributes.Get("name")); - menu.CreateChildDescriptor(_serializationManager, attributes); + menu.CreateChildDescriptor(serializationManager, attributes); return true; } @@ -188,7 +192,7 @@ private bool TryGetAttribute(out string? element, [NotNullWhen(true)] out string Token attributeToken = Current(); if (Check(_attributeTokenTypes)) { - while(Check(TokenType.DMF_Period)) { // element.attribute=value + while(Check(TokenType.Period)) { // element.attribute=value element ??= ""; if(element.Length > 0) element += "."; element += attributeToken.Text; @@ -201,21 +205,23 @@ private bool TryGetAttribute(out string? element, [NotNullWhen(true)] out string } } - if (!Check(TokenType.DMF_Equals)) { - ReuseToken(attributeToken); + if (!Check(TokenType.Equals)) { + // Ew + _tokenQueue.Enqueue(_currentToken); + _currentToken = attributeToken; return false; } Token attributeValue = Current(); string valueText = attributeValue.Text; - if (Check(TokenType.DMF_Period)) { // hidden verbs start with a period + if (Check(TokenType.Period)) { // hidden verbs start with a period attributeValue = Current(); valueText += attributeValue.Text; - if (!Check(TokenType.DMF_Value) && !Check(TokenType.DMF_Attribute)) + if (!Check(TokenType.Value) && !Check(TokenType.Attribute)) Error($"Invalid attribute value ({valueText})"); - } else if (!Check(TokenType.DMF_Value)) - if(Check(TokenType.DMF_Semicolon)) //thing.attribute=; means thing.attribute=empty string + } else if (!Check(TokenType.Value)) + if(Check(TokenType.Semicolon)) //thing.attribute=; means thing.attribute=empty string valueText = ""; else Error($"Invalid attribute value ({valueText})"); @@ -248,7 +254,52 @@ public MappingDataNode Attributes() { } private void Newline() { - while (Check(TokenType.Newline) || Check(TokenType.DMF_Semicolon)) { + while (Check(TokenType.Newline) || Check(TokenType.Semicolon)) { + } + } + + private void Error(string errorMessage) { + if (_errorMode) + return; + + _errorMode = true; + Errors.Add(errorMessage); + } + + private Token Current() { + return _currentToken; + } + + private Token Advance() { + _currentToken = (_tokenQueue.Count > 0) ? _tokenQueue.Dequeue() : lexer.NextToken(); + while (_currentToken.Type is TokenType.Error) { + Error(_currentToken.Text); + _currentToken = (_tokenQueue.Count > 0) ? _tokenQueue.Dequeue() : lexer.NextToken(); } + + return Current(); + } + + private bool Check(TokenType type) { + if (_currentToken.Type != type) + return false; + + Advance(); + return true; + } + + private bool Check(TokenType[] types) { + if (!types.Contains(_currentToken.Type)) + return false; + + Advance(); + return true; + } + + private void Consume(TokenType type, string errorMessage) { + if (Check(type)) + return; + + Error(errorMessage); } } diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index d39e65a149..303c50986a 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -1,6 +1,5 @@ using System.IO; using System.Text; -using OpenDreamShared.Compiler; using OpenDreamShared.Network.Messages; using OpenDreamClient.Interface.Controls; using OpenDreamClient.Interface.Descriptors; @@ -79,25 +78,15 @@ private set { public void LoadInterfaceFromSource(string source) { Reset(); - DMFLexer dmfLexer = new DMFLexer("interface.dmf", source); + DMFLexer dmfLexer = new DMFLexer(source); DMFParser dmfParser = new DMFParser(dmfLexer, _serializationManager); + InterfaceDescriptor interfaceDescriptor = dmfParser.Interface(); - InterfaceDescriptor? interfaceDescriptor = null; - try { - interfaceDescriptor = dmfParser.Interface(); - } catch (CompileErrorException) { } - - int errorCount = 0; - foreach (CompilerEmission warning in dmfParser.Emissions) { - if (warning.Level == ErrorLevel.Error) { - _sawmill.Error(warning.ToString()); - errorCount++; - } else { - _sawmill.Warning(warning.ToString()); + if (dmfParser.Errors.Count > 0) { + foreach (string error in dmfParser.Errors) { + _sawmill.Error(error); } - } - if (interfaceDescriptor == null || errorCount > 0) { // Open an error message that disconnects from the server once closed OpenAlert( "Error", @@ -190,7 +179,7 @@ private void RxAlert(MsgAlert message) { (responseType, response) => OnPromptFinished(message.PromptId, responseType, response)); } - public void OpenAlert(string title, string message, string button1, string? button2, string? button3, Action? onClose) { + public void OpenAlert(string title, string message, string button1, string? button2, string? button3, Action? onClose) { var alert = new AlertWindow( title, message, @@ -203,19 +192,19 @@ public void OpenAlert(string title, string message, string button1, string? butt private void RxPrompt(MsgPrompt pPrompt) { PromptWindow? prompt = null; - bool canCancel = (pPrompt.Types & DMValueType.Null) == DMValueType.Null; + bool canCancel = (pPrompt.Types & DreamValueType.Null) == DreamValueType.Null; - void OnPromptClose(DMValueType responseType, object? response) { + void OnPromptClose(DreamValueType responseType, object? response) { OnPromptFinished(pPrompt.PromptId, responseType, response); } - if ((pPrompt.Types & DMValueType.Text) == DMValueType.Text) { + if ((pPrompt.Types & DreamValueType.Text) == DreamValueType.Text) { prompt = new TextPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } else if ((pPrompt.Types & DMValueType.Num) == DMValueType.Num) { + } else if ((pPrompt.Types & DreamValueType.Num) == DreamValueType.Num) { prompt = new NumberPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } else if ((pPrompt.Types & DMValueType.Message) == DMValueType.Message) { + } else if ((pPrompt.Types & DreamValueType.Message) == DreamValueType.Message) { prompt = new MessagePrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } else if ((pPrompt.Types & DMValueType.Color) == DMValueType.Color) { + } else if ((pPrompt.Types & DreamValueType.Color) == DreamValueType.Color) { prompt = new ColorPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); } @@ -288,7 +277,7 @@ private void RxWinExists(MsgWinExists message) { InterfaceElement? element = FindElementWithId(message.ControlId); MsgPromptResponse response = new() { PromptId = message.PromptId, - Type = DMValueType.Text, + Type = DreamValueType.Text, Value = element?.Type ?? string.Empty }; @@ -300,7 +289,7 @@ private void RxWinGet(MsgWinGet message) { _timerManager.AddTimer(new Timer(100, false, () => { MsgPromptResponse response = new() { PromptId = message.PromptId, - Type = DMValueType.Text, + Type = DreamValueType.Text, Value = WinGet(message.ControlId, message.QueryValue) }; @@ -470,25 +459,18 @@ public void StopRepeatingCommand(string command) { } public void WinSet(string? controlId, string winsetParams) { - DMFLexer lexer = new DMFLexer($"winset({controlId}, \"{winsetParams}\")", winsetParams); + DMFLexer lexer = new DMFLexer(winsetParams); DMFParser parser = new DMFParser(lexer, _serializationManager); bool CheckParserErrors() { - if (parser.Emissions.Count > 0) { - bool hadError = false; - foreach (CompilerEmission emission in parser.Emissions) { - if (emission.Level == ErrorLevel.Error) { - _sawmill.Error(emission.ToString()); - hadError = true; - } else { - _sawmill.Warning(emission.ToString()); - } - } + if (parser.Errors.Count <= 0) + return false; - return hadError; + foreach (string error in parser.Errors) { + _sawmill.Error(error); } - return false; + return true; } if (string.IsNullOrEmpty(controlId)) { @@ -716,8 +698,8 @@ private void LoadDescriptor(ElementDescriptor descriptor) { } } - private void OnPromptFinished(int promptId, DMValueType responseType, object? response) { - var msg = new MsgPromptResponse() { + private void OnPromptFinished(int promptId, DreamValueType responseType, object? response) { + var msg = new MsgPromptResponse { PromptId = promptId, Type = responseType, Value = response diff --git a/OpenDreamClient/Interface/Prompts/AlertWindow.cs b/OpenDreamClient/Interface/Prompts/AlertWindow.cs index 011fe42d6d..0d3f95f538 100644 --- a/OpenDreamClient/Interface/Prompts/AlertWindow.cs +++ b/OpenDreamClient/Interface/Prompts/AlertWindow.cs @@ -5,7 +5,7 @@ namespace OpenDreamClient.Interface.Prompts; internal sealed class AlertWindow : PromptWindow { - public AlertWindow(string title, string message, string button1, string? button2, string? button3, Action? onClose) : + public AlertWindow(string title, string message, string button1, string? button2, string? button3, Action? onClose) : base(title, message, onClose) { CreateButton(button1, true); if (!string.IsNullOrEmpty(button2)) CreateButton(button2, false); @@ -13,7 +13,7 @@ public AlertWindow(string title, string message, string button1, string? button2 } protected override void ButtonClicked(string button) { - FinishPrompt(DMValueType.Text, button); + FinishPrompt(DreamValueType.Text, button); base.ButtonClicked(button); } diff --git a/OpenDreamClient/Interface/Prompts/ColorPrompt.cs b/OpenDreamClient/Interface/Prompts/ColorPrompt.cs index 81dc53e04c..072f0ddee6 100644 --- a/OpenDreamClient/Interface/Prompts/ColorPrompt.cs +++ b/OpenDreamClient/Interface/Prompts/ColorPrompt.cs @@ -1,5 +1,4 @@ using System.Linq; -using Linguini.Bundle.Errors; using OpenDreamShared.Dream; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; @@ -17,7 +16,7 @@ internal sealed class ColorPrompt : InputWindow { private readonly Color _originalColor; public ColorPrompt(string title, string message, string defaultValue, bool canCancel, - Action? onClose, bool alpha = false) : base(title, message, canCancel, onClose) { + Action? onClose, bool alpha = false) : base(title, message, canCancel, onClose) { _originalColor = Color.FromHex(defaultValue, Color.White); _colorSelector = new() { Color = _originalColor, @@ -26,6 +25,7 @@ public ColorPrompt(string title, string message, string defaultValue, bool canCa IsAlphaVisible = alpha, OnColorChanged = ColorSelectorSliders_OnColorChanged }; + var defaultHex = _colorSelector.IsAlphaVisible ? _originalColor.ToHex() : _originalColor.ToHexNoAlpha(); _hexColor = new LineEdit { Text = defaultHex, @@ -76,7 +76,7 @@ private void LineEdit_OnFinishInput(LineEdit.LineEditEventArgs args) { } protected override void OkButtonClicked() { - FinishPrompt(DMValueType.Color, _colorSelector.Color); + FinishPrompt(DreamValueType.Color, _colorSelector.Color); } } diff --git a/OpenDreamClient/Interface/Prompts/InputWindow.cs b/OpenDreamClient/Interface/Prompts/InputWindow.cs index 57c0bd18be..c0b853f978 100644 --- a/OpenDreamClient/Interface/Prompts/InputWindow.cs +++ b/OpenDreamClient/Interface/Prompts/InputWindow.cs @@ -6,7 +6,7 @@ namespace OpenDreamClient.Interface.Prompts; [Virtual] internal class InputWindow : PromptWindow { protected InputWindow(string title, string message, bool canCancel, - Action? onClose) : base(title, message, onClose) { + Action? onClose) : base(title, message, onClose) { CreateButton("Ok", true); if (canCancel) CreateButton("Cancel", false); } @@ -19,7 +19,7 @@ protected void SetPromptControl(Control promptControl, bool grabKeyboard = true) protected override void ButtonClicked(string button) { if (button == "Ok") OkButtonClicked(); - else FinishPrompt(DMValueType.Null, null); + else FinishPrompt(DreamValueType.Null, null); base.ButtonClicked(button); } diff --git a/OpenDreamClient/Interface/Prompts/ListPrompt.cs b/OpenDreamClient/Interface/Prompts/ListPrompt.cs index c76aeb0fb9..8f6b63cf33 100644 --- a/OpenDreamClient/Interface/Prompts/ListPrompt.cs +++ b/OpenDreamClient/Interface/Prompts/ListPrompt.cs @@ -9,7 +9,7 @@ internal sealed class ListPrompt : InputWindow { private readonly ItemList _itemList; public ListPrompt(string title, string message, string defaultValue, bool canCancel, string[] values, - Action? onClose) : base(title, message, canCancel, onClose) { + Action? onClose) : base(title, message, canCancel, onClose) { _itemList = new(); bool foundDefault = false; @@ -35,7 +35,7 @@ public ListPrompt(string title, string message, string defaultValue, bool canCan if (!item.Selected) continue; - FinishPrompt(DMValueType.Num, (float)_itemList.IndexOf(item)); + FinishPrompt(DreamValueType.Num, (float)_itemList.IndexOf(item)); return; } diff --git a/OpenDreamClient/Interface/Prompts/MessagePrompt.cs b/OpenDreamClient/Interface/Prompts/MessagePrompt.cs index 4c8a80a75d..8876d00fff 100644 --- a/OpenDreamClient/Interface/Prompts/MessagePrompt.cs +++ b/OpenDreamClient/Interface/Prompts/MessagePrompt.cs @@ -8,7 +8,7 @@ internal sealed class MessagePrompt : InputWindow { private readonly TextEdit _textEdit; public MessagePrompt(string title, string message, string defaultValue, bool canCancel, - Action? onClose) : base(title, message, canCancel, onClose) { + Action? onClose) : base(title, message, canCancel, onClose) { _textEdit = new TextEdit { TextRope = new Rope.Leaf(defaultValue), @@ -21,6 +21,6 @@ public MessagePrompt(string title, string message, string defaultValue, bool can } protected override void OkButtonClicked() { - FinishPrompt(DMValueType.Message, Rope.Collapse(_textEdit.TextRope)); + FinishPrompt(DreamValueType.Message, Rope.Collapse(_textEdit.TextRope)); } } diff --git a/OpenDreamClient/Interface/Prompts/NumberPrompt.cs b/OpenDreamClient/Interface/Prompts/NumberPrompt.cs index a03237e417..a7da6c444e 100644 --- a/OpenDreamClient/Interface/Prompts/NumberPrompt.cs +++ b/OpenDreamClient/Interface/Prompts/NumberPrompt.cs @@ -7,7 +7,7 @@ internal sealed class NumberPrompt : InputWindow { private readonly LineEdit _numberInput; public NumberPrompt(string title, string message, string defaultValue, bool canCancel, - Action? onClose) : base(title, message, canCancel, onClose) { + Action? onClose) : base(title, message, canCancel, onClose) { _numberInput = new() { Text = defaultValue, VerticalAlignment = VAlignment.Top, @@ -23,7 +23,7 @@ protected override void OkButtonClicked() { Logger.GetSawmill("opendream.prompt").Error($"Error while trying to convert {_numberInput.Text} to a number."); } - FinishPrompt(DMValueType.Num, num); + FinishPrompt(DreamValueType.Num, num); } private void NumberInput_TextEntered(LineEdit.LineEditEventArgs obj) { diff --git a/OpenDreamClient/Interface/Prompts/PromptWindow.cs b/OpenDreamClient/Interface/Prompts/PromptWindow.cs index d1118a2f89..d9a6026bc0 100644 --- a/OpenDreamClient/Interface/Prompts/PromptWindow.cs +++ b/OpenDreamClient/Interface/Prompts/PromptWindow.cs @@ -13,9 +13,9 @@ public abstract class PromptWindow : OSWindow { private readonly BoxContainer _buttonPanel; private bool _promptFinished; - private readonly Action? _closeAction; + private readonly Action? _closeAction; - protected PromptWindow(string? title, string? message, Action? onClose) { + protected PromptWindow(string? title, string? message, Action? onClose) { _closeAction = onClose; Title = !string.IsNullOrEmpty(title) ? title : "OpenDream"; @@ -68,7 +68,7 @@ protected virtual void ButtonClicked(string button) { Close(); } - protected void FinishPrompt(DMValueType responseType, object? value) { + protected void FinishPrompt(DreamValueType responseType, object? value) { if (_promptFinished) return; _promptFinished = true; diff --git a/OpenDreamClient/Interface/Prompts/TextPrompt.cs b/OpenDreamClient/Interface/Prompts/TextPrompt.cs index 43ad8ee6b7..67da9fd3b0 100644 --- a/OpenDreamClient/Interface/Prompts/TextPrompt.cs +++ b/OpenDreamClient/Interface/Prompts/TextPrompt.cs @@ -7,7 +7,7 @@ internal sealed class TextPrompt : InputWindow { private readonly LineEdit _textEdit; public TextPrompt(string title, string message, string defaultValue, bool canCancel, - Action? onClose) : base(title, message, canCancel, onClose) { + Action? onClose) : base(title, message, canCancel, onClose) { _textEdit = new LineEdit { Text = defaultValue, VerticalAlignment = VAlignment.Top @@ -18,7 +18,7 @@ public TextPrompt(string title, string message, string defaultValue, bool canCan } protected override void OkButtonClicked() { - FinishPrompt(DMValueType.Text, _textEdit.Text); + FinishPrompt(DreamValueType.Text, _textEdit.Text); } private void TextEdit_TextEntered(LineEdit.LineEditEventArgs e) { diff --git a/OpenDreamClient/Program.cs b/OpenDreamClient/Program.cs index e3faebdeea..fba48695e0 100644 --- a/OpenDreamClient/Program.cs +++ b/OpenDreamClient/Program.cs @@ -1,11 +1,11 @@ using Robust.Client; -namespace OpenDreamClient { - internal static class Program { - public static void Main(string[] args) { - ContentStart.StartLibrary(args, new GameControllerOptions() { - Sandboxing = true, - }); - } +namespace OpenDreamClient; + +internal static class Program { + public static void Main(string[] args) { + ContentStart.StartLibrary(args, new GameControllerOptions { + Sandboxing = true + }); } } diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 6bc43f2579..9db17f88a4 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -1,12 +1,12 @@ using System.Threading.Tasks; using System.Web; +using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs.Native; using OpenDreamRuntime.Rendering; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; using OpenDreamShared.Network.Messages; using Robust.Shared.Enums; using Robust.Shared.Player; @@ -249,10 +249,10 @@ public void HandleMsgPromptResponse(MsgPromptResponse message) { } DreamValue value = message.Type switch { - DMValueType.Null => DreamValue.Null, - DMValueType.Text or DMValueType.Message => new DreamValue((string)message.Value), - DMValueType.Num => new DreamValue((float)message.Value), - DMValueType.Color => new DreamValue(((Color)message.Value).ToHexNoAlpha()), + 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 + "'") }; @@ -362,18 +362,18 @@ public void HandleCommand(string fullCommand) { // TODO: this should probably be done on the client, shouldn't it? if (args.Length == 1) { // No args given; prompt the client for them for (int i = 0; i < verb.ArgumentNames.Count; i++) { - String argumentName = verb.ArgumentNames[i]; - DMValueType argumentType = verb.ArgumentTypes[i]; - DreamValue argumentValue = await Prompt(argumentType, title: String.Empty, // No settable title for verbs - argumentName, defaultValue: String.Empty); // No default value for verbs + string argumentName = verb.ArgumentNames[i]; + DreamValueType argumentType = verb.ArgumentTypes[i]; + DreamValue argumentValue = await Prompt(argumentType, title: string.Empty, // No settable title for verbs + argumentName, defaultValue: string.Empty); // No default value for verbs arguments[i] = argumentValue; } } else { // Attempt to parse the given arguments for (int i = 0; i < verb.ArgumentNames.Count; i++) { - DMValueType argumentType = verb.ArgumentTypes[i]; + DreamValueType argumentType = verb.ArgumentTypes[i]; - if (argumentType == DMValueType.Text) { + if (argumentType == DreamValueType.Text) { arguments[i] = new(args[i + 1]); } else { _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); @@ -395,9 +395,9 @@ public void HandleCommand(string fullCommand) { } } - public Task Prompt(DMValueType types, String title, String message, String defaultValue) { + public Task Prompt(DreamValueType types, string title, string message, string defaultValue) { var task = MakePromptTask(out var promptId); - var msg = new MsgPrompt() { + var msg = new MsgPrompt { PromptId = promptId, Title = title, Message = message, @@ -409,20 +409,18 @@ public Task Prompt(DMValueType types, String title, String message, return task; } - public async Task PromptList(DMValueType types, DreamList list, string title, string message, DreamValue defaultValue) { + public async Task PromptList(DreamValueType types, DreamList list, string title, string message, DreamValue defaultValue) { List listValues = list.GetValues(); List promptValues = new(listValues.Count); - for (int i = 0; i < listValues.Count; i++) { - DreamValue value = listValues[i]; - - if (types.HasFlag(DMValueType.Obj) && !value.TryGetValueAsDreamObject(out _)) + foreach (var value in listValues) { + if (types.HasFlag(DreamValueType.Obj) && !value.TryGetValueAsDreamObject(out _)) continue; - if (types.HasFlag(DMValueType.Mob) && !value.TryGetValueAsDreamObject(out _)) + if (types.HasFlag(DreamValueType.Mob) && !value.TryGetValueAsDreamObject(out _)) continue; - if (types.HasFlag(DMValueType.Turf) && !value.TryGetValueAsDreamObject(out _)) + if (types.HasFlag(DreamValueType.Turf) && !value.TryGetValueAsDreamObject(out _)) continue; - if (types.HasFlag(DMValueType.Area) && !value.TryGetValueAsDreamObject(out _)) + if (types.HasFlag(DreamValueType.Area) && !value.TryGetValueAsDreamObject(out _)) continue; promptValues.Add(value.Stringify()); @@ -432,11 +430,11 @@ public async Task PromptList(DMValueType types, DreamList list, stri return DreamValue.Null; var task = MakePromptTask(out var promptId); - var msg = new MsgPromptList() { + var msg = new MsgPromptList { PromptId = promptId, Title = title, Message = message, - CanCancel = (types & DMValueType.Null) == DMValueType.Null, + CanCancel = (types & DreamValueType.Null) == DreamValueType.Null, DefaultValue = defaultValue.Stringify(), Values = promptValues.ToArray() }; diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index dad94d06e4..1a610ccf9e 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Text.Json; using DMCompiler.Bytecode; +using DMCompiler.Json; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; @@ -10,7 +11,6 @@ using OpenDreamRuntime.Resources; using OpenDreamShared; using OpenDreamShared.Dream; -using OpenDreamShared.Json; using Robust.Server; using Robust.Server.Player; using Robust.Server.ServerStatus; diff --git a/OpenDreamRuntime/DreamMapManager.cs b/OpenDreamRuntime/DreamMapManager.cs index c189f65675..9cddcf7cc7 100644 --- a/OpenDreamRuntime/DreamMapManager.cs +++ b/OpenDreamRuntime/DreamMapManager.cs @@ -1,11 +1,11 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using DMCompiler.Json; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Rendering; using OpenDreamShared.Dream; -using OpenDreamShared.Json; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Utility; diff --git a/OpenDreamRuntime/DreamThread.cs b/OpenDreamRuntime/DreamThread.cs index 8e1ea7bb71..d6770f6514 100644 --- a/OpenDreamRuntime/DreamThread.cs +++ b/OpenDreamRuntime/DreamThread.cs @@ -1,11 +1,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.DebugAdapter; using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; namespace OpenDreamRuntime { public enum ProcStatus { @@ -29,7 +29,7 @@ public abstract class DreamProc { public readonly List? ArgumentNames; - public readonly List? ArgumentTypes; + public readonly List? ArgumentTypes; public string VerbName => _verbName ?? Name; public readonly string? VerbCategory = string.Empty; @@ -38,7 +38,7 @@ public abstract class DreamProc { 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, string? verbName, string? verbCategory, string? verbDesc, sbyte invisibility, bool isVerb = false) { Id = id; OwningType = owningType; Name = name; diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index eaba0d09a8..6a2ba39e87 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -1,13 +1,12 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Threading.Tasks; +using DMCompiler.Json; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.DebugAdapter; using OpenDreamRuntime.Rendering; using OpenDreamRuntime.Resources; -using OpenDreamShared.Dream; -using OpenDreamShared.Json; using Robust.Server.GameObjects; using Robust.Server.GameStates; using Robust.Server.Player; diff --git a/OpenDreamRuntime/Procs/AsyncNativeProc.cs b/OpenDreamRuntime/Procs/AsyncNativeProc.cs index ff86a77f2d..585206c5d2 100644 --- a/OpenDreamRuntime/Procs/AsyncNativeProc.cs +++ b/OpenDreamRuntime/Procs/AsyncNativeProc.cs @@ -1,10 +1,9 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Resources; -using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; using Dependency = Robust.Shared.IoC.DependencyAttribute; namespace OpenDreamRuntime.Procs { diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index ecd3ddce4c..08e096cf73 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -2196,7 +2196,7 @@ public static ProcStatus OutputControl(DMProcState state) { } public static ProcStatus Prompt(DMProcState state) { - DMValueType types = (DMValueType)state.ReadInt(); + DreamValueType types = (DreamValueType)state.ReadInt(); DreamValue list = state.Pop(); DreamValue message, title, defaultValue; diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index 35e834d453..e42feb8ee9 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -3,13 +3,13 @@ using System.Runtime.CompilerServices; using System.Text; using DMCompiler.Bytecode; +using DMCompiler.DM; +using DMCompiler.Json; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Procs.DebugAdapter; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; -using OpenDreamShared.Json; namespace OpenDreamRuntime.Procs { public sealed class DMProc : DreamProc { @@ -132,12 +132,12 @@ private bool CheckIfNullProc() { } } - private static List? GetArgumentTypes(ProcDefinitionJson json) { + private static List GetArgumentTypes(ProcDefinitionJson json) { if (json.Arguments == null) { return new(); } else { - var argumentTypes = new List(json.Arguments.Count); - argumentTypes.AddRange(json.Arguments.Select(a => a.Type)); + var argumentTypes = new List(json.Arguments.Count); + argumentTypes.AddRange(json.Arguments.Select(a => (DreamValueType)a.Type)); return argumentTypes; } } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index b111ac6325..f1fb4827df 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -15,6 +15,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; +using DMCompiler.DM; using OpenDreamRuntime.Objects.Types; using DreamValueType = OpenDreamRuntime.DreamValue.DreamValueType; using DreamValueTypeFlag = OpenDreamRuntime.DreamValue.DreamValueTypeFlag; diff --git a/OpenDreamRuntime/Procs/NativeProc.cs b/OpenDreamRuntime/Procs/NativeProc.cs index 66384711b0..e3333f5940 100644 --- a/OpenDreamRuntime/Procs/NativeProc.cs +++ b/OpenDreamRuntime/Procs/NativeProc.cs @@ -1,10 +1,9 @@ using System.Reflection; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; +using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Resources; -using OpenDreamShared.Dream; -using OpenDreamShared.Dream.Procs; namespace OpenDreamRuntime.Procs; diff --git a/OpenDreamRuntime/Procs/ProcDecoder.cs b/OpenDreamRuntime/Procs/ProcDecoder.cs index 3386663015..fe53a769e2 100644 --- a/OpenDreamRuntime/Procs/ProcDecoder.cs +++ b/OpenDreamRuntime/Procs/ProcDecoder.cs @@ -5,16 +5,10 @@ namespace OpenDreamRuntime.Procs; -public struct ProcDecoder { - public readonly IReadOnlyList Strings; - public readonly byte[] Bytecode; - public int Offset; - - public ProcDecoder(IReadOnlyList strings, byte[] bytecode) { - Strings = strings; - Bytecode = bytecode; - Offset = 0; - } +public struct ProcDecoder(IReadOnlyList strings, byte[] bytecode) { + public readonly IReadOnlyList Strings = strings; + public readonly byte[] Bytecode = bytecode; + public int Offset = 0; public bool Remaining => Offset < Bytecode.Length; @@ -32,8 +26,8 @@ public int ReadInt() { return value; } - public DMValueType ReadValueType() { - return (DMValueType) ReadInt(); + public DreamValueType ReadValueType() { + return (DreamValueType) ReadInt(); } public float ReadFloat() { diff --git a/OpenDreamShared/Compiler/AST.cs b/OpenDreamShared/Compiler/AST.cs deleted file mode 100644 index f8a717cb45..0000000000 --- a/OpenDreamShared/Compiler/AST.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace OpenDreamShared.Compiler { - public interface ASTNode where VisitorType:ASTVisitor { - public void Visit(VisitorType visitor); - } -} diff --git a/OpenDreamShared/Compiler/ASTVisitor.cs b/OpenDreamShared/Compiler/ASTVisitor.cs deleted file mode 100644 index b6ef6b81bf..0000000000 --- a/OpenDreamShared/Compiler/ASTVisitor.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace OpenDreamShared.Compiler { - public interface ASTVisitor { - - } -} diff --git a/OpenDreamShared/Compiler/CompilerError.cs b/OpenDreamShared/Compiler/CompilerError.cs deleted file mode 100644 index d5f9e299d3..0000000000 --- a/OpenDreamShared/Compiler/CompilerError.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using Robust.Shared.Analyzers; - -namespace OpenDreamShared.Compiler { - - /// - /// All values should be unique. - /// - public enum WarningCode { - // 0 - 999 are reserved for giving codes to fatal errors which cannot reasonably be demoted to a warning/notice/disable. - Unknown = 0, - BadToken = 1, - BadDirective = 10, - BadExpression = 11, - MissingExpression = 12, - BadLabel = 19, - InvalidReference = 50, - BadArgument = 100, - InvalidArgumentKey = 101, - ArglistOnlyArgument = 102, - HardReservedKeyword = 200, // For keywords that CANNOT be un-reserved. - ItemDoesntExist = 404, - DanglingOverride = 405, - StaticOverride = 406, - IAmATeaPot = 418, // TODO: Implement the HTCPC protocol for OD - HardConstContext = 500, - WriteToConstant = 501, - InvalidInclusion = 900, - - // 1000 - 1999 are reserved for preprocessor configuration. - FileAlreadyIncluded = 1000, - MissingIncludedFile = 1001, - MisplacedDirective = 1100, - UndefineMissingDirective = 1101, - DefinedMissingParen = 1150, - ErrorDirective = 1200, - WarningDirective = 1201, - MiscapitalizedDirective = 1300, - - // 2000 - 2999 are reserved for compiler configuration of actual behaviour. - SoftReservedKeyword = 2000, // For keywords that SHOULD be reserved, but don't have to be. 'null' and 'defined', for instance - DuplicateVariable = 2100, - DuplicateProcDefinition = 2101, - TooManyArguments = 2200, - PointlessParentCall = 2205, - PointlessBuiltinCall = 2206, // For pointless calls to issaved() or initial() - SuspiciousMatrixCall = 2207, // Calling matrix() with seemingly the wrong arguments - FallbackBuiltinArgument = 2208, // A builtin (sin(), cos(), etc) with an invalid/fallback argument - MalformedRange = 2300, - InvalidRange = 2301, - InvalidSetStatement = 2302, - InvalidOverride = 2303, - DanglingVarType = 2401, // For types inferred by a particular var definition and nowhere else, that ends up not existing (not forced-fatal because BYOND doesn't always error) - MissingInterpolatedExpression = 2500, // A text macro is missing a required interpolated expression - AmbiguousResourcePath = 2600, - - // 3000 - 3999 are reserved for stylistic configuration. - EmptyBlock = 3100, - EmptyProc = 3101, - UnsafeClientAccess = 3200, - SuspiciousSwitchCase = 3201, // "else if" cases are actually valid DM, they just spontaneously end the switch context and begin an if-else ladder within the else case of the switch - AssignmentInConditional = 3202, - - // 4000 - 4999 are reserved for runtime configuration. (TODO: Runtime doesn't know about configs yet!) - } - - public enum ErrorLevel - { - //When this warning is emitted: - Disabled, // Nothing happens. - Notice, // Nothing happens unless the user provides a '--wall' argument. - Warning, // A warning is always emitted. - Error // An error is always emitted. - } - - /// - /// Stores the location and message of a notice/warning/error. - /// - public struct CompilerEmission { - public ErrorLevel Level; - public WarningCode Code; - public Location Location; - public string Message; - - public CompilerEmission(ErrorLevel level, Location? location, string message) { - Level = level; - Code = WarningCode.Unknown; - Location = location ?? Location.Unknown; - Message = message; - } - - public CompilerEmission(ErrorLevel level, WarningCode code, Location? location, string message) { - Level = level; - Code = code; - Location = location ?? Location.Unknown; - Message = message; - } - - public override string ToString() => Level switch { - ErrorLevel.Disabled => "", - ErrorLevel.Notice => $"Notice OD{(int)Code:d4} at {Location.ToString()}: {Message}", - ErrorLevel.Warning => $"Warning OD{(int)Code:d4} at {Location.ToString()}: {Message}", - ErrorLevel.Error => $"Error OD{(int)Code:d4} at {Location.ToString()}: {Message}", - _ => "", - }; - } - - [Virtual] - [Obsolete("This is not a desirable way for the compiler to emit an error. Use CompileAbortException or ForceError() if it needs to be fatal, or an DMCompiler.Emit() otherwise.")] - public class CompileErrorException : Exception { - public CompilerEmission Error; - public CompileErrorException(CompilerEmission error) : base(error.Message) - { - Error = error; - } - public CompileErrorException(Location location, string message) : base(message) { - Error = new CompilerEmission(ErrorLevel.Error, location, message); - } - - public CompileErrorException(string message) { - Error = new CompilerEmission(ErrorLevel.Error, Location.Unknown, message); - } - } - - - /// - /// Represents an internal compiler error that should cause parsing for a particular block to cease.
- /// This should be ideally used for exceptions that are the fault of the compiler itself,
- /// like an abnormal state being reached or something. - ///
- public sealed class CompileAbortException : CompileErrorException - { - public CompileAbortException(CompilerEmission error) : base(error) {} - public CompileAbortException(Location location, string message) : base(location, message) {} - - public CompileAbortException(string message) : base(message) {} - } - - public sealed class UnknownIdentifierException : CompileErrorException { - public string IdentifierName; - - public UnknownIdentifierException(Location location, string identifierName) : base(location, $"Unknown identifier \"{identifierName}\"") { - IdentifierName = identifierName; - } - } -} diff --git a/OpenDreamShared/Compiler/Lexer.cs b/OpenDreamShared/Compiler/Lexer.cs deleted file mode 100644 index 762f484d37..0000000000 --- a/OpenDreamShared/Compiler/Lexer.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using Robust.Shared.Analyzers; -using System.Collections.Generic; -using System.IO; - -namespace OpenDreamShared.Compiler { - [Virtual] - public class Lexer { - public Location CurrentLocation { get; protected set; } - public string SourceName { get; protected set; } - public IEnumerable Source { get; protected set; } - public bool AtEndOfSource { get; protected set; } = false; - - protected Queue _pendingTokenQueue = new(); - - private IEnumerator _sourceEnumerator; - private SourceType _current; - - protected Lexer(string sourceName, IEnumerable source) { - CurrentLocation = new Location(sourceName, 1, 0); - SourceName = sourceName; - Source = source; - if (source == null) - throw new FileNotFoundException("Source file could not be read: " + sourceName); - _sourceEnumerator = Source.GetEnumerator(); - } - - public Token GetNextToken() { - if (_pendingTokenQueue.Count > 0) - return _pendingTokenQueue.Dequeue(); - - Token nextToken = ParseNextToken(); - while (nextToken.Type == TokenType.Skip) nextToken = ParseNextToken(); - - if (_pendingTokenQueue.Count > 0) { - _pendingTokenQueue.Enqueue(nextToken); - return _pendingTokenQueue.Dequeue(); - } else { - return nextToken; - } - } - - protected virtual Token ParseNextToken() { - return CreateToken(TokenType.Unknown, GetCurrent()?.ToString() ?? String.Empty); - } - - protected Token CreateToken(TokenType type, string text, object? value = null) { - return new Token(type, text, CurrentLocation, value); - } - - protected Token CreateToken(TokenType type, char text, object? value = null) { - return CreateToken(type, char.ToString(text), value); - } - - protected virtual SourceType GetCurrent() { - return _current; - } - - protected virtual SourceType Advance() { - if (_sourceEnumerator.MoveNext()) { - _current = _sourceEnumerator.Current; - } else { - AtEndOfSource = true; - } - - return GetCurrent(); - } - } - - [Virtual] - public class TextLexer : Lexer { - protected string _source; - protected int _currentPosition = 0; - - public TextLexer(string sourceName, string source) : base(sourceName, source) { - _source = source; - - Advance(); - } - - protected override Token ParseNextToken() { - char c = GetCurrent(); - - Token token; - switch (c) { - case '\n': token = CreateToken(TokenType.Newline, c); Advance(); break; - case '\0': token = CreateToken(TokenType.EndOfFile, c); Advance(); break; - default: token = CreateToken(TokenType.Unknown, c); break; - } - - return token; - } - - protected override char GetCurrent() { - if (AtEndOfSource) return '\0'; - else return base.GetCurrent(); - } - - protected override char Advance() { - if (GetCurrent() == '\n') { - CurrentLocation = new Location( - CurrentLocation.SourceFile, - CurrentLocation.Line + 1, - 1 - ); - } else { - CurrentLocation = new Location( - CurrentLocation.SourceFile, - CurrentLocation.Line, - CurrentLocation.Column + 1 - ); - } - - _currentPosition++; - return base.Advance(); - } - } - - [Virtual] - public class TokenLexer : Lexer { - public TokenLexer(string sourceName, IEnumerable source) : base(sourceName, source) { - Advance(); - } - - protected override Token Advance() { - Token current = base.Advance(); - - //Warnings and errors go straight to output, no processing - while (current.Type is TokenType.Warning or TokenType.Error && !AtEndOfSource) { - _pendingTokenQueue.Enqueue(current); - current = base.Advance(); - } - - CurrentLocation = current.Location; - return current; - } - } -} diff --git a/OpenDreamShared/Compiler/Location.cs b/OpenDreamShared/Compiler/Location.cs deleted file mode 100644 index 648f58d3b3..0000000000 --- a/OpenDreamShared/Compiler/Location.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text; - -namespace OpenDreamShared.Compiler; - -public readonly struct Location { - /// - /// For when DM location information can't be determined. - /// - public static readonly Location Unknown = new Location(); - /// - /// For when internal OpenDream warnings/errors are raised or something internal needs to be passed a location. - /// - public static readonly Location Internal = new Location("", null, null); - - public Location(string filePath, int? line, int? column) { - SourceFile = filePath; - Line = line; - Column = column; - } - - public readonly string SourceFile { get; } - public readonly int? Line { get; } - public readonly int? Column { get; } - - public override string ToString() { - var builder = new StringBuilder(SourceFile ?? ""); - - if (Line is not null && Line is var line) { - builder.Append(":" + line); - - if (Column is not null && Column is var column) { - builder.Append(":" + column); - } - } - - return builder.ToString(); - } -} diff --git a/OpenDreamShared/Compiler/Parser.cs b/OpenDreamShared/Compiler/Parser.cs deleted file mode 100644 index fee91a837b..0000000000 --- a/OpenDreamShared/Compiler/Parser.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Robust.Shared.Analyzers; -using System.Collections.Generic; -using JetBrains.Annotations; - -namespace OpenDreamShared.Compiler { - [Virtual] - public partial class Parser { - /// Includes errors and warnings acccumulated by this parser. - /// These initial capacities are arbitrary. We just assume there's a decent chance you'll get a handful of errors/warnings. - public List Emissions = new(8); - - protected Lexer _lexer; - private Token _currentToken; - private readonly Stack _tokenStack = new(1); - /// The maximum number of errors or warnings we'd ever place into . - protected const int MAX_EMISSIONS_RECORDED = 50_000_000; - - protected Parser(Lexer lexer) { - _lexer = lexer; - - Advance(); - } - - /// - /// Does not consume; this is simply a friendly getter. - /// - protected Token Current() { - return _currentToken; - } - - protected virtual Token Advance() { - if (_tokenStack.Count > 0) { - _currentToken = _tokenStack.Pop(); - } else { - _currentToken = _lexer.GetNextToken(); - - if (_currentToken.Type == TokenType.Error) { - Error((string)_currentToken.Value!, throwException: false); - Advance(); - } else if (_currentToken.Type == TokenType.Warning) { - Warning((string)_currentToken.Value!); - Advance(); - } - } - - return Current(); - } - - protected void ReuseToken(Token token) { - _tokenStack.Push(_currentToken); - _currentToken = token; - } - - protected bool Check(TokenType type) { - if (Current().Type == type) { - Advance(); - - return true; - } - - return false; - } - - protected bool Check(TokenType[] types) { - TokenType currentType = Current().Type; - foreach (TokenType type in types) { - if (currentType == type) { - Advance(); - - return true; - } - } - - return false; - } - - protected void Consume(TokenType type, string errorMessage) { - if (!Check(type)) { - Error(errorMessage); - } - } - - /// The that was found. - protected TokenType Consume(TokenType[] types, string errorMessage) { - foreach (TokenType type in types) { - if (Check(type)) return type; - } - - Error(errorMessage); - return TokenType.Unknown; - } - - /// - /// Emits an error discovered during parsing, optionally causing a throw. - /// - /// This implementation on does not make use of
- /// since there are some parsers that aren't always in the compilation context, like the ones for DMF and DMM.
- ///
- [AssertionMethod] - protected void Error(string message, [AssertionCondition(AssertionConditionType.IS_TRUE)] bool throwException = true) { - CompilerEmission error = new CompilerEmission(ErrorLevel.Error, _currentToken.Location, message); - - if(Emissions.Count < MAX_EMISSIONS_RECORDED) - Emissions.Add(error); - if (throwException) - throw new CompileErrorException(error); - } - - /// - /// Emits a warning discovered during parsing, optionally causing a throw. - /// - /// This implementation on does not make use of
- /// since there are some parsers that aren't always in the compilation context, like the ones for DMF and DMM.
- ///
- protected void Warning(string message, Token? token = null) { - token ??= _currentToken; - Emissions.Add(new CompilerEmission(ErrorLevel.Warning, token?.Location, message)); - } - } -} diff --git a/OpenDreamShared/Compiler/Token.cs b/OpenDreamShared/Compiler/Token.cs deleted file mode 100644 index baa98c2b83..0000000000 --- a/OpenDreamShared/Compiler/Token.cs +++ /dev/null @@ -1,180 +0,0 @@ -// ReSharper disable InconsistentNaming - -namespace OpenDreamShared.Compiler { - // Must be : byte for ReadOnlySpan x = new TokenType[] { } to be intrinsic'd by the compiler. - public enum TokenType : byte { - //Base lexer - Error, - Warning, - Unknown, - Skip, //Internally skipped by the lexer - - //Text lexer - Newline, - EndOfFile, - - //DM Preprocessor - DM_Preproc_ConstantString, - DM_Preproc_Define, - DM_Preproc_Else, - DM_Preproc_EndIf, - DM_Preproc_Error, - DM_Preproc_Identifier, - DM_Preproc_If, - DM_Preproc_Ifdef, - DM_Preproc_Ifndef, - DM_Preproc_Elif, - DM_Preproc_Include, - DM_Preproc_LineSplice, - DM_Preproc_Number, - DM_Preproc_ParameterStringify, - DM_Preproc_Pragma, - DM_Preproc_Punctuator, - DM_Preproc_Punctuator_Colon, - DM_Preproc_Punctuator_Comma, - DM_Preproc_Punctuator_LeftBracket, - DM_Preproc_Punctuator_LeftParenthesis, - DM_Preproc_Punctuator_Period, - DM_Preproc_Punctuator_Question, - DM_Preproc_Punctuator_RightBracket, - DM_Preproc_Punctuator_RightParenthesis, - DM_Preproc_Punctuator_Semicolon, - DM_Preproc_StringBegin, - DM_Preproc_StringMiddle, - DM_Preproc_StringEnd, - DM_Preproc_TokenConcat, - DM_Preproc_Undefine, - DM_Preproc_Warning, - DM_Preproc_Whitespace, - - //DM - DM_And, - DM_AndAnd, - DM_AndEquals, - DM_AndAndEquals, - DM_As, - DM_AssignInto, - DM_Bar, - DM_BarBar, - DM_BarEquals, - DM_BarBarEquals, - DM_Break, - DM_Call, - DM_Catch, - DM_Colon, - DM_Comma, - DM_ConstantString, - DM_Continue, - DM_Dedent, - DM_Del, - DM_Do, - DM_DoubleSquareBracket, - DM_DoubleSquareBracketEquals, - DM_Else, - DM_Equals, - DM_EqualsEquals, - DM_Exclamation, - DM_ExclamationEquals, - DM_Float, - DM_For, - DM_Goto, - DM_GreaterThan, - DM_GreaterThanEquals, - DM_Identifier, - DM_If, - DM_In, - DM_Indent, - DM_IndeterminateArgs, - DM_RightShift, - DM_RightShiftEquals, - DM_Integer, - DM_LeftBracket, - DM_LeftCurlyBracket, - DM_LeftParenthesis, - DM_LeftShift, - DM_LeftShiftEquals, - DM_LessThan, - DM_LessThanEquals, - DM_Minus, - DM_MinusEquals, - DM_MinusMinus, - DM_Modulus, - DM_ModulusEquals, - DM_ModulusModulus, - DM_ModulusModulusEquals, - DM_New, - DM_Null, - DM_Period, - DM_Plus, - DM_PlusEquals, - DM_PlusPlus, - DM_Proc, - DM_Question, - DM_QuestionColon, - DM_QuestionLeftBracket, - DM_QuestionPeriod, - DM_RawString, - DM_Resource, - DM_Return, - DM_RightBracket, - DM_RightCurlyBracket, - DM_RightParenthesis, - DM_Semicolon, - DM_Set, - DM_Slash, - DM_SlashEquals, - DM_Spawn, - DM_Star, - DM_StarEquals, - DM_StarStar, - DM_Step, - DM_StringBegin, - DM_StringMiddle, - DM_StringEnd, - DM_SuperProc, - DM_Switch, - DM_Throw, - DM_Tilde, - DM_TildeEquals, - DM_TildeExclamation, - DM_To, - DM_Try, - DM_Var, - DM_While, - DM_Whitespace, - DM_Xor, - DM_XorEquals, - - //DMF - DMF_Attribute, - DMF_Elem, - DMF_Equals, - DMF_Macro, - DMF_Menu, - DMF_Period, - DMF_Semicolon, - DMF_Value, - DMF_Window - } - - public struct Token { - public readonly TokenType Type; - public Location Location; - /// Use if you intend to show this to the user. - public readonly string Text; - public readonly object? Value; - - public string PrintableText => Text.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\t", "\\t"); - - public Token(TokenType type, string text, Location location, object? value) { - Type = type; - Text = text; - Location = location; - Value = value; - } - - public override string ToString() { - return $"{Type}({Location.ToString()}, {PrintableText})"; - } - } -} diff --git a/OpenDreamShared/Dream/ATOMType.cs b/OpenDreamShared/Dream/ATOMType.cs deleted file mode 100644 index 4f93de6bf1..0000000000 --- a/OpenDreamShared/Dream/ATOMType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OpenDreamShared.Dream { - public enum AtomType { - Atom = 0x0, - Area = 0x1, - Turf = 0x2, - Movable = 0x3 - } -} diff --git a/OpenDreamShared/Dream/ColorHelpers.cs b/OpenDreamShared/Dream/ColorHelpers.cs index 93f8898249..c210a2cdef 100644 --- a/OpenDreamShared/Dream/ColorHelpers.cs +++ b/OpenDreamShared/Dream/ColorHelpers.cs @@ -1,49 +1,47 @@ using Robust.Shared.Maths; -using System; using System.Collections.Generic; -namespace OpenDreamShared.Dream -{ - public static class ColorHelpers - { - public static readonly Dictionary Colors = new() { - { "black", new Color(00, 00, 00) }, - { "silver", new Color(192, 192, 192) }, - { "gray", new Color(128, 128, 128) }, - { "grey", new Color(128, 128, 128) }, - { "white", new Color(255, 255, 255) }, - { "maroon", new Color(128, 0, 0) }, - { "red", new Color(255, 0, 0) }, - { "purple", new Color(128, 0, 128) }, - { "fuchsia", new Color(255, 0, 255) }, - { "magenta", new Color(255, 0, 255) }, - { "green", new Color(0, 192, 0) }, - { "lime", new Color(0, 255, 0) }, - { "olive", new Color(128, 128, 0) }, - { "gold", new Color(128, 128, 0) }, - { "yellow", new Color(255, 255, 0) }, - { "navy", new Color(0, 0, 128) }, - { "blue", new Color(0, 0, 255) }, - { "teal", new Color(0, 128, 128) }, - { "aqua", new Color(0, 255, 255) }, - { "cyan", new Color(0, 255, 255) } - }; +namespace OpenDreamShared.Dream; - public static bool TryParseColor(string color, out Color colorOut, string defaultAlpha = "ff") { - if (color.StartsWith("#")) { - if (color.Length == 4 || color.Length == 5) { //4-bit color; repeat each digit - string alphaComponent = (color.Length == 5) ? new string(color[4], 2) : defaultAlpha; +public static class ColorHelpers { + public static readonly Dictionary Colors = new() { + {"black", new Color(00, 00, 00)}, + {"silver", new Color(192, 192, 192)}, + {"gray", new Color(128, 128, 128)}, + {"grey", new Color(128, 128, 128)}, + {"white", new Color(255, 255, 255)}, + {"maroon", new Color(128, 0, 0)}, + {"red", new Color(255, 0, 0)}, + {"purple", new Color(128, 0, 128)}, + {"fuchsia", new Color(255, 0, 255)}, + {"magenta", new Color(255, 0, 255)}, + {"green", new Color(0, 192, 0)}, + {"lime", new Color(0, 255, 0)}, + {"olive", new Color(128, 128, 0)}, + {"gold", new Color(128, 128, 0)}, + {"yellow", new Color(255, 255, 0)}, + {"navy", new Color(0, 0, 128)}, + {"blue", new Color(0, 0, 255)}, + {"teal", new Color(0, 128, 128)}, + {"aqua", new Color(0, 255, 255)}, + {"cyan", new Color(0, 255, 255)} + }; - color = new string('#', 1) + new string(color[1], 2) + new string(color[2], 2) + new string(color[3], 2) + alphaComponent; - } else if (color.Length == 7) { //Missing alpha - color += defaultAlpha; - } + public static bool TryParseColor(string color, out Color colorOut, string defaultAlpha = "ff") { + if (color.StartsWith("#")) { + if (color.Length == 4 || color.Length == 5) { //4-bit color; repeat each digit + string alphaComponent = (color.Length == 5) ? new string(color[4], 2) : defaultAlpha; - colorOut = Color.FromHex(color, Color.White); - return true; + color = new string('#', 1) + new string(color[1], 2) + new string(color[2], 2) + + new string(color[3], 2) + alphaComponent; + } else if (color.Length == 7) { //Missing alpha + color += defaultAlpha; } - return Colors.TryGetValue(color.ToLower(), out colorOut); + colorOut = Color.FromHex(color, Color.White); + return true; } + + return Colors.TryGetValue(color.ToLower(), out colorOut); } } diff --git a/OpenDreamShared/Dream/DreamValueType.cs b/OpenDreamShared/Dream/DreamValueType.cs new file mode 100644 index 0000000000..ff481d41e7 --- /dev/null +++ b/OpenDreamShared/Dream/DreamValueType.cs @@ -0,0 +1,32 @@ +using System; + +namespace OpenDreamShared.Dream; + +// If you are modifying this, you must also modify DMCompiler.DM.DMValueType !! +// Unfortunately the client needs this and it can't reference DMCompiler due to the sandbox + +/// +///Stores any explicit casting done via the "as" keyword. Also stores compiler hints for DMStandard.
+///is a [Flags] enum because it's possible for something to have multiple values (especially with the quirky DMStandard ones) +///
+[Flags] +public enum DreamValueType { + Anything = 0x0, + Null = 0x1, + Text = 0x2, + Obj = 0x4, + Mob = 0x8, + Turf = 0x10, + Num = 0x20, + Message = 0x40, + Area = 0x80, + Color = 0x100, + File = 0x200, + CommandText = 0x400, + Sound = 0x800, + Icon = 0x1000, + + //Byond here be dragons + Unimplemented = 0x2000, // Marks that a method or property is not implemented. Throws a compiler warning if accessed. + CompiletimeReadonly = 0x4000, // Marks that a property can only ever be read from, never written to. This is a const-ier version of const, for certain standard values like list.type +} diff --git a/OpenDreamShared/Json/DreamMapJson.cs b/OpenDreamShared/Json/DreamMapJson.cs deleted file mode 100644 index 2813690e16..0000000000 --- a/OpenDreamShared/Json/DreamMapJson.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace OpenDreamShared.Json { - public sealed class DreamMapJson { - public int MaxX { get; set; } - public int MaxY { get; set; } - public int MaxZ { get; set; } - public Dictionary CellDefinitions { get; set; } = new(); - public List Blocks { get; set; } = new(); - } - - public sealed class CellDefinitionJson { - public string Name { get; set; } - public MapObjectJson Turf { get; set; } - public MapObjectJson Area { get; set; } - public List Objects { get; set; } = new(); - - public CellDefinitionJson(string name) { - Name = name; - } - } - - public sealed class MapObjectJson { - public int Type { get; set; } - public Dictionary? VarOverrides { get; set; } - - public MapObjectJson(int type) { - Type = type; - } - - public bool AddVarOverride(string varName, object varValue) { - VarOverrides ??= new(); - bool contained = VarOverrides.ContainsKey(varName); - VarOverrides[varName] = varValue; - return !contained; - } - - public override bool Equals(object? obj) { - return obj is MapObjectJson json && - Type == json.Type && - EqualityComparer>.Default.Equals(VarOverrides, json.VarOverrides); - } - - public override int GetHashCode() { - return HashCode.Combine(Type, VarOverrides); - } - } - - public sealed class MapBlockJson { - public int X { get; set; } - public int Y { get; set; } - public int Z { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public List Cells { get; set; } = new(); - - public MapBlockJson(int x, int y, int z) { - X = x; - Y = y; - Z = z; - } - } -} diff --git a/OpenDreamShared/Json/DreamObjectJson.cs b/OpenDreamShared/Json/DreamObjectJson.cs deleted file mode 100644 index f48aa7b87a..0000000000 --- a/OpenDreamShared/Json/DreamObjectJson.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; - -namespace OpenDreamShared.Json { - public enum JsonVariableType { - Resource = 0, - Type = 1, - Proc = 2, - List = 3, - PositiveInfinity = 4, - NegativeInfinity = 5 - } - - public sealed class DreamTypeJson { - public string Path { get; set; } - public int? Parent { get; set; } - public int? InitProc { get; set; } - public List> Procs { get; set; } - public List Verbs { get; set; } - public Dictionary Variables { get; set; } - public Dictionary GlobalVariables { get; set; } - public HashSet? ConstVariables { get; set; } - public HashSet? TmpVariables { get; set; } - } - - public sealed class GlobalListJson { - public int GlobalCount { get; set; } - public List Names { get; set; } - public Dictionary Globals { get; set; } - } -} diff --git a/OpenDreamShared/Json/DreamProcJson.cs b/OpenDreamShared/Json/DreamProcJson.cs deleted file mode 100644 index ef0aa66d1d..0000000000 --- a/OpenDreamShared/Json/DreamProcJson.cs +++ /dev/null @@ -1,39 +0,0 @@ -using OpenDreamShared.Dream.Procs; -using System.Collections.Generic; -using OpenDreamShared.Dream; - -namespace OpenDreamShared.Json { - public sealed class ProcDefinitionJson { - public int OwningTypeId { get; set; } - public string Name { get; set; } - public bool IsVerb { get; set; } - public int MaxStackSize { get; set; } - public List? Arguments { get; set; } - public List Locals { get; set; } - public ProcAttributes Attributes { get; set; } = ProcAttributes.None; - public List SourceInfo { get; set; } - public byte[]? Bytecode { get; set; } - - public string? VerbName { get; set; } - public string? VerbCategory { get; set; } = null; - public string? VerbDesc { get; set; } - public sbyte Invisibility { get; set; } - } - - public sealed class ProcArgumentJson { - public string Name { get; set; } - public DMValueType Type { get; set; } - } - - public sealed class LocalVariableJson { - public int Offset { get; set; } - public int? Remove { get; set; } - public string Add { get; set; } - } - - public sealed class SourceInfoJson { - public int Offset { get; set; } - public int? File { get; set; } - public int Line { get; set; } - } -} diff --git a/OpenDreamShared/Network/Messages/MsgPrompt.cs b/OpenDreamShared/Network/Messages/MsgPrompt.cs index d5642037ea..2332b8e196 100644 --- a/OpenDreamShared/Network/Messages/MsgPrompt.cs +++ b/OpenDreamShared/Network/Messages/MsgPrompt.cs @@ -1,33 +1,32 @@ -using System; -using Lidgren.Network; +using Lidgren.Network; using OpenDreamShared.Dream; using Robust.Shared.Network; using Robust.Shared.Serialization; -namespace OpenDreamShared.Network.Messages { - public sealed class MsgPrompt : NetMessage { - public override MsgGroups MsgGroup => MsgGroups.EntityEvent; +namespace OpenDreamShared.Network.Messages; - public int PromptId; - public DMValueType Types; - public string Title = String.Empty; - public string Message = String.Empty; - public string DefaultValue = String.Empty; +public sealed class MsgPrompt : NetMessage { + public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - PromptId = buffer.ReadVariableInt32(); - Types = (DMValueType) buffer.ReadUInt16(); - Title = buffer.ReadString(); - Message = buffer.ReadString(); - DefaultValue = buffer.ReadString(); - } + public int PromptId; + public DreamValueType Types; + public string Title = string.Empty; + public string Message = string.Empty; + public string DefaultValue = string.Empty; - public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.WriteVariableInt32(PromptId); - buffer.Write((ushort) Types); - buffer.Write(Title); - buffer.Write(Message); - buffer.Write(DefaultValue); - } + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + PromptId = buffer.ReadVariableInt32(); + Types = (DreamValueType) buffer.ReadUInt16(); + Title = buffer.ReadString(); + Message = buffer.ReadString(); + DefaultValue = buffer.ReadString(); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { + buffer.WriteVariableInt32(PromptId); + buffer.Write((ushort) Types); + buffer.Write(Title); + buffer.Write(Message); + buffer.Write(DefaultValue); } } diff --git a/OpenDreamShared/Network/Messages/MsgPromptResponse.cs b/OpenDreamShared/Network/Messages/MsgPromptResponse.cs index 8acbe4f254..1f7b336631 100644 --- a/OpenDreamShared/Network/Messages/MsgPromptResponse.cs +++ b/OpenDreamShared/Network/Messages/MsgPromptResponse.cs @@ -5,48 +5,48 @@ using Robust.Shared.Network; using Robust.Shared.Serialization; -namespace OpenDreamShared.Network.Messages { - public sealed class MsgPromptResponse : NetMessage { - public override MsgGroups MsgGroup => MsgGroups.EntityEvent; +namespace OpenDreamShared.Network.Messages; - public int PromptId; - public DMValueType Type; - public object? Value; +public sealed class MsgPromptResponse : NetMessage { + public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - PromptId = buffer.ReadVariableInt32(); - Type = (DMValueType)buffer.ReadUInt16(); + public int PromptId; + public DreamValueType Type; + public object? Value; - Value = Type switch { - DMValueType.Null => null, - DMValueType.Text or DMValueType.Message => buffer.ReadString(), - DMValueType.Num => buffer.ReadSingle(), - DMValueType.Color => new Color(buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte()), - _ => throw new ArgumentOutOfRangeException() - }; - } + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + PromptId = buffer.ReadVariableInt32(); + Type = (DreamValueType)buffer.ReadUInt16(); + + Value = Type switch { + DreamValueType.Null => null, + DreamValueType.Text or DreamValueType.Message => buffer.ReadString(), + DreamValueType.Num => buffer.ReadSingle(), + DreamValueType.Color => new Color(buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte()), + _ => throw new ArgumentOutOfRangeException() + }; + } - public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.WriteVariableInt32(PromptId); + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { + buffer.WriteVariableInt32(PromptId); - buffer.Write((ushort)Type); - switch (Type) { - case DMValueType.Null: break; - case DMValueType.Text or DMValueType.Message: - buffer.Write((string)Value!); - break; - case DMValueType.Num: - buffer.Write((float)Value!); - break; - case DMValueType.Color: - var color = (Color)Value!; - buffer.Write(color.RByte); - buffer.Write(color.GByte); - buffer.Write(color.BByte); - buffer.Write(color.AByte); - break; - default: throw new Exception("Invalid prompt response type '" + Type + "'"); - } + buffer.Write((ushort)Type); + switch (Type) { + case DreamValueType.Null: break; + case DreamValueType.Text or DreamValueType.Message: + buffer.Write((string)Value!); + break; + case DreamValueType.Num: + buffer.Write((float)Value!); + break; + case DreamValueType.Color: + var color = (Color)Value!; + buffer.Write(color.RByte); + buffer.Write(color.GByte); + buffer.Write(color.BByte); + buffer.Write(color.AByte); + break; + default: throw new Exception("Invalid prompt response type '" + Type + "'"); } } } From b319b4f2bfe44e6b5bfe88a09ff5a845bbcb55b9 Mon Sep 17 00:00:00 2001 From: ike709 Date: Wed, 24 Jan 2024 14:26:25 -0700 Subject: [PATCH 57/64] Fix `roll(sides)` (#1631) Co-authored-by: ike709 --- .../Procs/Native/DreamProcNativeRoot.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index f1fb4827df..17c3fe2e53 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -2073,21 +2073,24 @@ public static DreamValue NativeProc_roll(NativeProc.Bundle bundle, DreamObject? int sides; int modifier = 0; if (bundle.Arguments.Length == 1) { - if(!bundle.GetArgument(0, "ndice").TryGetValueAsString(out var diceInput)) { - return new DreamValue(1); - } - - string[] diceList = diceInput.Split('d'); - if (diceList.Length < 2) { - if (!Int32.TryParse(diceList[0], out sides)) { throw new Exception($"Invalid dice value: {diceInput}"); } - } else { - if (!Int32.TryParse(diceList[0], out dice)) { throw new Exception($"Invalid dice value: {diceInput}"); } - if (!Int32.TryParse(diceList[1], out sides)) { - string[] sideList = diceList[1].Split('+'); + var arg = bundle.GetArgument(0, "ndice"); + if(arg.TryGetValueAsString(out var diceInput)) { + string[] diceList = diceInput.Split('d'); + if (diceList.Length < 2) { + if (!Int32.TryParse(diceList[0], out sides)) { throw new Exception($"Invalid dice value: {diceInput}"); } + } else { + if (!Int32.TryParse(diceList[0], out dice)) { throw new Exception($"Invalid dice value: {diceInput}"); } + if (!Int32.TryParse(diceList[1], out sides)) { + string[] sideList = diceList[1].Split('+'); - if (!Int32.TryParse(sideList[0], out sides) || !Int32.TryParse(sideList[1], out modifier)) - throw new Exception($"Invalid dice value: {diceInput}"); + if (!Int32.TryParse(sideList[0], out sides) || !Int32.TryParse(sideList[1], out modifier)) + throw new Exception($"Invalid dice value: {diceInput}"); + } } + } else if (arg.IsNull) { + return new DreamValue(1); + } else if (!arg.TryGetValueAsInteger(out sides)) { + throw new Exception($"Invalid dice value: {arg}"); } } else if (!bundle.GetArgument(0, "ndice").TryGetValueAsInteger(out dice) || !bundle.GetArgument(1, "sides").TryGetValueAsInteger(out sides)) { return new DreamValue(0); From df74600a1c0705a51c0904ab8c19a468dc004ddd Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 25 Jan 2024 11:35:49 -0500 Subject: [PATCH 58/64] Set `TGS_TEST_GITHUB_TOKEN` in the TGS test workflow (#1636) --- .github/workflows/test-tgs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-tgs.yml b/.github/workflows/test-tgs.yml index a6a34ad7b4..ba2195c21b 100644 --- a/.github/workflows/test-tgs.yml +++ b/.github/workflows/test-tgs.yml @@ -14,6 +14,7 @@ env: OD_DOTNET_VERSION: 8 TGS_DOTNET_VERSION: 8 TGS_REFERENCE: dev + TGS_TEST_GITHUB_TOKEN: ${{ secrets.TGS_TEST_GITHUB_TOKEN }} jobs: build: From 9c467549edbff0e8c8ec6b9deae5fa2a03ee5e97 Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 25 Jan 2024 20:21:00 -0500 Subject: [PATCH 59/64] Add a workflow job for packaging latest OpenDream as a release (#1637) Co-authored-by: pali <6pali6@gmail.com> --- .github/workflows/build-test.yml | 39 ++++++++++++++++++++++++- OpenDreamPackageTool/ServerPackaging.cs | 2 +- OpenDreamPackageTool/TgsPackaging.cs | 1 + 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d6dc02d690..e0dd7c1778 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,4 +1,4 @@ -name: Build & Test +name: Build, Test & Release on: push: @@ -43,3 +43,40 @@ jobs: run: | $env:COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n + - name: Publish OpenDream + if: github.event_name == 'push' + run: dotnet run --project OpenDreamPackageTool --no-build --configuration Release -- --server --hybrid-acz --platform ${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }} --output output/ + - name: Publish DMCompiler + if: github.event_name == 'push' + run: dotnet publish DMCompiler -c Release -o output/DMCompiler_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }} + - name: Gzip releases + if: github.event_name == 'push' + run: | + tar -czvf output/DMCompiler_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }}.tar.gz -C output DMCompiler_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }} + tar -czvf output/OpenDreamServer_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }}.tar.gz -C output OpenDreamServer_${{ matrix.os == 'windows-latest' && 'win-x64' || 'linux-x64' }} + - name: Upload artifact + if: github.event_name == 'push' + uses: actions/upload-artifact@v3 + with: + name: build-${{ matrix.os }} + path: output/*.tar.gz + retention-days: 1 + + release: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + - name: Publish latest release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "latest" + prerelease: true + title: "Development Build" + files: | + artifacts/*/*.tar.gz diff --git a/OpenDreamPackageTool/ServerPackaging.cs b/OpenDreamPackageTool/ServerPackaging.cs index bda62bde52..e0273a9b93 100644 --- a/OpenDreamPackageTool/ServerPackaging.cs +++ b/OpenDreamPackageTool/ServerPackaging.cs @@ -12,9 +12,9 @@ public static class ServerPackaging { new("win-x64", "Windows", true), new("linux-x64", "Linux", true), new("linux-arm64", "Linux", true), - new("osx-x64", "MacOS", true), // Non-default platforms (i.e. for Watchdog Git) + new("osx-x64", "MacOS", false), // macOS is not supported currently new("win-x86", "Windows", false), new("linux-x86", "Linux", false), new("linux-arm", "Linux", false), diff --git a/OpenDreamPackageTool/TgsPackaging.cs b/OpenDreamPackageTool/TgsPackaging.cs index c65f6c0f2c..183be3d6e7 100644 --- a/OpenDreamPackageTool/TgsPackaging.cs +++ b/OpenDreamPackageTool/TgsPackaging.cs @@ -50,6 +50,7 @@ public static void Package(Program.TgsOptions options) { } private static void PublishCompiler(string platformRId, string targetOs) { + // TODO: Add a --compiler option to the package tool ProcessHelpers.RunCheck(new ProcessStartInfo { FileName = "dotnet", ArgumentList = { From fe6c1a890b007ae62ecf3a1a720d4598ab1f7b83 Mon Sep 17 00:00:00 2001 From: wixoa Date: Sat, 27 Jan 2024 18:05:16 -0500 Subject: [PATCH 60/64] Complete rework of verbs (#1633) --- .../DMProject/Tests}/verb_duplicate.dm | 2 +- Content.IntegrationTests/DMProject/code.dm | 1 + .../DMProject/environment.dme | 1 + OpenDreamClient/ClientVerbSystem.cs | 179 ++++++++++++++++++ .../Input/ContextMenu/ContextMenuItem.xaml | 2 +- .../Input/ContextMenu/ContextMenuItem.xaml.cs | 66 +++---- .../ContextMenu/ContextMenuPopup.xaml.cs | 89 ++++++--- .../Input/ContextMenu/VerbMenuPopup.xaml | 2 +- .../Input/ContextMenu/VerbMenuPopup.xaml.cs | 63 ++++-- OpenDreamClient/Input/MouseInputSystem.cs | 6 +- .../Interface/Controls/ControlInfo.cs | 56 ++++-- .../Interface/DebugWindows/MacrosWindow.cs | 2 - .../Interface/DebugWindows/VerbsWindow.cs | 121 ++++++++++++ .../Interface/DreamInterfaceManager.cs | 117 +++++++----- .../Interface/DummyDreamInterfaceManager.cs | 6 +- OpenDreamClient/Interface/InterfaceMacro.cs | 12 +- OpenDreamRuntime/AtomManager.cs | 48 +++-- OpenDreamRuntime/DreamConnection.cs | 114 +---------- OpenDreamRuntime/DreamManager.Connections.cs | 1 - OpenDreamRuntime/DreamManager.cs | 17 ++ OpenDreamRuntime/DreamThread.cs | 3 +- OpenDreamRuntime/Input/DreamCommandSystem.cs | 5 - OpenDreamRuntime/Input/MouseInputSystem.cs | 21 +- OpenDreamRuntime/Objects/DreamObject.cs | 1 + .../Objects/DreamObjectDefinition.cs | 5 +- OpenDreamRuntime/Objects/DreamObjectTree.cs | 14 +- OpenDreamRuntime/Objects/Types/DreamList.cs | 119 ++++++++++-- .../Objects/Types/DreamObjectAtom.cs | 17 +- .../Objects/Types/DreamObjectClient.cs | 19 +- .../Objects/Types/DreamObjectTurf.cs | 2 - OpenDreamRuntime/ServerVerbSystem.cs | 147 ++++++++++++++ ...mReference.cs => ClientObjectReference.cs} | 17 +- OpenDreamShared/Dream/IconAppearance.cs | 27 ++- OpenDreamShared/Dream/VerbSystem.cs | 94 +++++++++ .../Input/SharedMouseInputSystem.cs | 12 +- .../Network/Messages/MsgCommand.cs | 19 +- .../Messages/MsgUpdateAvailableVerbs.cs | 33 ---- .../Rendering/SharedAppearanceSystem.cs | 48 ++--- 38 files changed, 1066 insertions(+), 442 deletions(-) rename {Content.Tests/DMProject/Tests/Procs => Content.IntegrationTests/DMProject/Tests}/verb_duplicate.dm (81%) create mode 100644 OpenDreamClient/ClientVerbSystem.cs create mode 100644 OpenDreamClient/Interface/DebugWindows/VerbsWindow.cs create mode 100644 OpenDreamRuntime/ServerVerbSystem.cs rename OpenDreamShared/Dream/{AtomReference.cs => ClientObjectReference.cs} (60%) create mode 100644 OpenDreamShared/Dream/VerbSystem.cs delete mode 100644 OpenDreamShared/Network/Messages/MsgUpdateAvailableVerbs.cs diff --git a/Content.Tests/DMProject/Tests/Procs/verb_duplicate.dm b/Content.IntegrationTests/DMProject/Tests/verb_duplicate.dm similarity index 81% rename from Content.Tests/DMProject/Tests/Procs/verb_duplicate.dm rename to Content.IntegrationTests/DMProject/Tests/verb_duplicate.dm index fbc176231e..b2a491282d 100644 --- a/Content.Tests/DMProject/Tests/Procs/verb_duplicate.dm +++ b/Content.IntegrationTests/DMProject/Tests/verb_duplicate.dm @@ -1,7 +1,7 @@ /mob/proc/test() return -/proc/RunTest() +/proc/test_verb_duplicate() var/mob/m = new m.verbs += /mob/proc/test m.verbs += /mob/proc/test diff --git a/Content.IntegrationTests/DMProject/code.dm b/Content.IntegrationTests/DMProject/code.dm index 2ea2a4439d..ebb99dd087 100644 --- a/Content.IntegrationTests/DMProject/code.dm +++ b/Content.IntegrationTests/DMProject/code.dm @@ -29,4 +29,5 @@ test_block() test_color_matrix() test_range() + test_verb_duplicate() world.log << "IntegrationTests successful, /world/New() exiting..." \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/environment.dme b/Content.IntegrationTests/DMProject/environment.dme index 48a75aaadb..a4abe97328 100644 --- a/Content.IntegrationTests/DMProject/environment.dme +++ b/Content.IntegrationTests/DMProject/environment.dme @@ -2,5 +2,6 @@ #include "Tests/block.dm" #include "Tests/color_matrix.dm" #include "Tests/range.dm" +#include "Tests/verb_duplicate.dm" #include "map.dmm" #include "interface.dmf" \ No newline at end of file diff --git a/OpenDreamClient/ClientVerbSystem.cs b/OpenDreamClient/ClientVerbSystem.cs new file mode 100644 index 0000000000..9d53745ef7 --- /dev/null +++ b/OpenDreamClient/ClientVerbSystem.cs @@ -0,0 +1,179 @@ +using System.Threading.Tasks; +using OpenDreamClient.Interface; +using OpenDreamClient.Rendering; +using OpenDreamShared.Dream; +using OpenDreamShared.Rendering; +using Robust.Client.Player; +using Robust.Shared.Asynchronous; +using Robust.Shared.Timing; + +namespace OpenDreamClient; + +public sealed class ClientVerbSystem : VerbSystem { + [Dependency] private readonly IDreamInterfaceManager _interfaceManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly ITaskManager _taskManager = default!; + [Dependency] private readonly ITimerManager _timerManager = default!; + + private EntityQuery _spriteQuery; + private EntityQuery _sightQuery; + + private readonly Dictionary _verbs = new(); + private List? _clientVerbs; + + public override void Initialize() { + _spriteQuery = _entityManager.GetEntityQuery(); + _sightQuery = _entityManager.GetEntityQuery(); + + _playerManager.LocalPlayerAttached += OnLocalPlayerAttached; + + SubscribeNetworkEvent(OnAllVerbsEvent); + SubscribeNetworkEvent(OnRegisterVerbEvent); + SubscribeNetworkEvent(OnUpdateClientVerbsEvent); + } + + public override void Shutdown() { + _verbs.Clear(); + _clientVerbs = null; + } + + /// + /// Prompt the user for the arguments to a verb, then ask the server to execute it + /// + /// The target of the verb + /// ID of the verb to execute + public async void ExecuteVerb(ClientObjectReference src, int verbId) { + var verbInfo = _verbs[verbId]; + + RaiseNetworkEvent(new ExecuteVerbEvent(src, verbId, await PromptVerbArguments(verbInfo))); + } + + /// + /// Ask the server to execute a verb with the given arguments + /// + /// The target of the verb + /// ID of the verb to execute + /// Arguments to the verb + /// The server will not execute the verb if the arguments are invalid // TODO: I think the server actually just errors, fix that + public void ExecuteVerb(ClientObjectReference src, int verbId, object?[] arguments) { + RaiseNetworkEvent(new ExecuteVerbEvent(src, verbId, arguments)); + } + + public IEnumerable GetAllVerbs() { + return _verbs.Values; + } + + /// + /// 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 + public IEnumerable<(int Id, ClientObjectReference Src, VerbInfo VerbInfo)> GetExecutableVerbs(bool ignoreHiddenAttr = false) { + DMISpriteComponent? playerSprite = null; + sbyte? seeInvisibility = null; + if (_playerManager.LocalEntity != null) { + playerSprite = _spriteQuery.GetComponent(_playerManager.LocalEntity.Value); + seeInvisibility = _sightQuery.GetComponent(_playerManager.LocalEntity.Value).SeeInvisibility; + } + + // First, the verbs attached to our client + if (_clientVerbs != null) { + foreach (var verbId in _clientVerbs) { + if (!_verbs.TryGetValue(verbId, out var verb)) + continue; + if (verb.IsHidden(ignoreHiddenAttr, seeInvisibility ?? 0)) + continue; // TODO: How do invisible client verbs work when you don't have a mob? + + yield return (verbId, ClientObjectReference.Client, verb); + } + } + + // Then, the verbs attached to our mob + if (playerSprite?.Icon.Appearance is { } playerAppearance) { + var playerNetEntity = _entityManager.GetNetEntity(_playerManager.LocalEntity); + + if (playerNetEntity != null) { + foreach (var verbId in playerAppearance.Verbs) { + if (!_verbs.TryGetValue(verbId, out var verb)) + continue; + if (verb.IsHidden(ignoreHiddenAttr, seeInvisibility!.Value)) + continue; + + yield return (verbId, new(playerNetEntity.Value), verb); + } + } + } + } + + public bool TryGetVerbInfo(int verbId, out VerbInfo verbInfo) { + return _verbs.TryGetValue(verbId, out verbInfo); + } + + /// + /// Look for a verb with the given command-name that the client can execute + /// + /// Command-name to look for + /// The ID, target, and verb information if a verb was found + public (int Id, ClientObjectReference Src, VerbInfo VerbInfo)? FindVerbWithCommandName(string commandName) { + foreach (var verb in GetExecutableVerbs(true)) { + if (verb.VerbInfo.GetCommandName() == commandName) + return verb; + } + + return null; + } + + /// + /// Open prompt windows for the user to enter the arguments to a verb + /// + /// The verb to get arguments for + /// The values the user gives + private async Task PromptVerbArguments(VerbInfo verbInfo) { + var argumentCount = verbInfo.Arguments.Length; + var arguments = (argumentCount > 0) ? new object?[argumentCount] : Array.Empty(); + + for (int i = 0; i < argumentCount; i++) { + var arg = verbInfo.Arguments[i]; + var tcs = new TaskCompletionSource(); + + _taskManager.RunOnMainThread(() => { + _interfaceManager.Prompt(arg.Types, verbInfo.Name, arg.Name, string.Empty, (_, value) => { + tcs.SetResult(value); + }); + }); + + arguments[i] = await tcs.Task; // Wait for this prompt to finish before moving on to the next + } + + return arguments; + } + + private void OnAllVerbsEvent(AllVerbsEvent e) { + _verbs.EnsureCapacity(e.Verbs.Count); + + for (int i = 0; i < e.Verbs.Count; i++) { + var verb = e.Verbs[i]; + + _verbs.Add(i, verb); + } + + _interfaceManager.DefaultInfo?.RefreshVerbs(this); + } + + private void OnRegisterVerbEvent(RegisterVerbEvent e) { + _verbs.Add(e.VerbId, e.VerbInfo); + } + + private void OnUpdateClientVerbsEvent(UpdateClientVerbsEvent e) { + _clientVerbs = e.VerbIds; + _interfaceManager.DefaultInfo?.RefreshVerbs(this); + } + + private void OnLocalPlayerAttached(EntityUid obj) { + // Our mob changed, update our verb panels + // A little hacky, but also wait half a second for verb information about our mob to arrive + // TODO: Remove this timer + _timerManager.AddTimer(new Timer(500, false, () => _interfaceManager.DefaultInfo?.RefreshVerbs(this))); + } +} diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml b/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml index fc2ce10e61..18a3aff41b 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml @@ -1,6 +1,6 @@ - + diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs index f3cd6feb5c..650afa6ba2 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs @@ -1,58 +1,46 @@ using OpenDreamClient.Rendering; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; -using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -namespace OpenDreamClient.Input.ContextMenu { - [GenerateTypedNameReferences] - public sealed partial class ContextMenuItem : PanelContainer { - private static readonly StyleBox HoverStyle = new StyleBoxFlat(Color.Gray); +namespace OpenDreamClient.Input.ContextMenu; - private readonly IUserInterfaceManager _uiManager; +[GenerateTypedNameReferences] +internal sealed partial class ContextMenuItem : PanelContainer { + private static readonly StyleBox HoverStyle = new StyleBoxFlat(Color.Gray); - private readonly MetaDataComponent? _entityMetaData; - private VerbMenuPopup? _currentVerbMenu; + public readonly EntityUid Entity; + public readonly MetaDataComponent EntityMetaData; + public readonly DMISpriteComponent? EntitySprite; - public ContextMenuItem(IUserInterfaceManager uiManager, IEntityManager entityManager, EntityUid entity) { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); + private readonly ContextMenuPopup _menu; - _uiManager = uiManager; + public ContextMenuItem(ContextMenuPopup menu, EntityUid entity, MetaDataComponent metadata, DMISpriteComponent sprite) { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); - NameLabel.Margin = new Thickness(2, 0, 4, 0); - if (entityManager.TryGetComponent(entity, out _entityMetaData)) { - NameLabel.Text = _entityMetaData.EntityName; - } + Entity = entity; + EntityMetaData = metadata; + EntitySprite = sprite; + _menu = menu; - Icon.Margin = new Thickness(2); - if (entityManager.TryGetComponent(entity, out DMISpriteComponent? sprite)) { - Icon.Texture = sprite.Icon.CurrentFrame; - } + NameLabel.Margin = new Thickness(2, 0, 4, 0); + NameLabel.Text = metadata.EntityName; - OnMouseEntered += MouseEntered; - OnMouseExited += MouseExited; - } - - private void MouseEntered(GUIMouseHoverEventArgs args) { - PanelOverride = HoverStyle; + Icon.Texture = sprite.Icon.CurrentFrame; + } - _currentVerbMenu = new VerbMenuPopup(_entityMetaData); - _uiManager.ModalRoot.AddChild(_currentVerbMenu); + protected override void MouseEntered() { + base.MouseEntered(); - Vector2 desiredSize = _currentVerbMenu.DesiredSize; - _currentVerbMenu.Open(UIBox2.FromDimensions(new Vector2(GlobalPosition.X + Size.X, GlobalPosition.Y), desiredSize)); - } + PanelOverride = HoverStyle; + _menu.SetActiveItem(this); + } - private void MouseExited(GUIMouseHoverEventArgs args) { - PanelOverride = null; + protected override void MouseExited() { + base.MouseExited(); - if (_currentVerbMenu != null) { - _currentVerbMenu.Close(); - _uiManager.ModalRoot.RemoveChild(_currentVerbMenu); - _currentVerbMenu = null; - } - } + PanelOverride = null; } } diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index d9520bb3c4..d12cad55c7 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -1,46 +1,81 @@ using OpenDreamClient.Rendering; using OpenDreamShared.Dream; +using OpenDreamShared.Rendering; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map; -namespace OpenDreamClient.Input.ContextMenu { - [GenerateTypedNameReferences] - public sealed partial class ContextMenuPopup : Popup { - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IUserInterfaceManager _uiManager = default!; - private readonly TransformSystem? _transformSystem; +namespace OpenDreamClient.Input.ContextMenu; - public int EntityCount => ContextMenu.ChildCount; +[GenerateTypedNameReferences] +internal sealed partial class ContextMenuPopup : Popup { + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IUserInterfaceManager _uiManager = default!; + private readonly TransformSystem? _transformSystem; + private readonly ClientVerbSystem? _verbSystem; - public ContextMenuPopup() { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); + public int EntityCount => ContextMenu.ChildCount; - _entitySystemManager.TryGetEntitySystem(out _transformSystem); - } + private VerbMenuPopup? _currentVerbMenu; + + public ContextMenuPopup() { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + _entitySystemManager.TryGetEntitySystem(out _transformSystem); + _entitySystemManager.TryGetEntitySystem(out _verbSystem); + } - public void RepopulateEntities(IEnumerable entities) { - ContextMenu.RemoveAllChildren(); + public void RepopulateEntities(IEnumerable entities) { + ContextMenu.RemoveAllChildren(); - if (_transformSystem == null) - return; + if (_transformSystem == null) + return; - foreach (EntityUid entity in entities) { - if (!_mapManager.IsGrid(_transformSystem.GetParent(entity).Owner)) // Not a child of another entity - continue; - if (!_entityManager.TryGetComponent(entity, out DMISpriteComponent? sprite)) // Has a sprite - continue; - if (sprite.Icon.Appearance.MouseOpacity == MouseOpacity.Transparent) // Not transparent to mouse clicks - continue; + foreach (EntityUid entity in entities) { + if (!_mapManager.IsGrid(_transformSystem.GetParent(entity).Owner)) // Not a child of another entity + continue; + if (!_entityManager.TryGetComponent(entity, out DMISpriteComponent? sprite)) // Has a sprite + continue; + if (sprite.Icon.Appearance.MouseOpacity == MouseOpacity.Transparent) // Not transparent to mouse clicks + continue; + + var metadata = _entityManager.GetComponent(entity); + + ContextMenu.AddChild(new ContextMenuItem(this, entity, metadata, sprite)); + } + } - ContextMenu.AddChild(new ContextMenuItem(_uiManager, _entityManager, entity)); - } + public void SetActiveItem(ContextMenuItem item) { + if (_currentVerbMenu != null) { + _currentVerbMenu.Close(); + _uiManager.ModalRoot.RemoveChild(_currentVerbMenu); } + + _currentVerbMenu = new VerbMenuPopup(_entityManager, _verbSystem, GetSeeInvisible(), item.Entity, item.EntityMetaData, item.EntitySprite); + + _currentVerbMenu.OnVerbSelected += Close; + + Vector2 desiredSize = _currentVerbMenu.DesiredSize; + Vector2 verbMenuPos = item.GlobalPosition with { X = item.GlobalPosition.X + item.Size.X }; + _uiManager.ModalRoot.AddChild(_currentVerbMenu); + _currentVerbMenu.Open(UIBox2.FromDimensions(verbMenuPos, desiredSize)); + } + + /// The see_invisible of our current mob + private sbyte GetSeeInvisible() { + if (_playerManager.LocalEntity == null) + return 0; + if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out DreamMobSightComponent? sight)) + return 0; + + return sight.SeeInvisibility; } } diff --git a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml index ead78e3e33..aa1cc65b7f 100644 --- a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml +++ b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml @@ -7,7 +7,7 @@ - + diff --git a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs index 9e33a06968..e75db414f4 100644 --- a/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/VerbMenuPopup.xaml.cs @@ -1,19 +1,60 @@ +using OpenDreamClient.Rendering; +using OpenDreamShared.Dream; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -namespace OpenDreamClient.Input.ContextMenu { - [GenerateTypedNameReferences] - public sealed partial class VerbMenuPopup : Popup { - public VerbMenuPopup(MetaDataComponent? entityMetaData) { - RobustXamlLoader.Load(this); - - if (entityMetaData != null && !string.IsNullOrEmpty(entityMetaData.EntityDescription)) { - DescLabel.Margin = new Thickness(4, 0, 4, 0); - DescLabel.Text = entityMetaData.EntityDescription; - } else { - Desc.Visible = false; +namespace OpenDreamClient.Input.ContextMenu; + +[GenerateTypedNameReferences] +internal sealed partial class VerbMenuPopup : Popup { + public delegate void VerbSelectedHandler(); + + public VerbSelectedHandler? OnVerbSelected; + + private readonly IEntityManager _entityManager; + private readonly ClientVerbSystem? _verbSystem; + + private readonly EntityUid _entity; + + public VerbMenuPopup(IEntityManager entityManager, ClientVerbSystem? verbSystem, sbyte seeInvisible, EntityUid entity, MetaDataComponent? entityMetaData, DMISpriteComponent? entitySprite) { + RobustXamlLoader.Load(this); + + _entityManager = entityManager; + _verbSystem = verbSystem; + _entity = entity; + + if (entityMetaData != null && !string.IsNullOrEmpty(entityMetaData.EntityDescription)) { + DescLabel.Margin = new Thickness(4, 0, 4, 0); + DescLabel.Text = entityMetaData.EntityDescription; + } else { + 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)) + continue; + + AddVerb(verbId, verbInfo); } } } + + private void AddVerb(int verbId, VerbSystem.VerbInfo verbInfo) { + var netEntity = _entityManager.GetNetEntity(_entity); + var button = new Button { + Text = verbInfo.Name + }; + + button.OnPressed += _ => { + _verbSystem?.ExecuteVerb(new(netEntity), verbId); + Close(); + OnVerbSelected?.Invoke(); + }; + + VerbMenu.AddChild(button); + } } diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index cf512dbb28..16d57a215d 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -27,8 +27,8 @@ internal sealed class MouseInputSystem : SharedMouseInputSystem { private ContextMenuPopup _contextMenu = default!; private EntityClickInformation? _selectedEntity; - private sealed class EntityClickInformation(AtomReference atom, ScreenCoordinates initialMousePos, ClickParams clickParams) { - public readonly AtomReference Atom = atom; + private sealed class EntityClickInformation(ClientObjectReference atom, ScreenCoordinates initialMousePos, ClickParams clickParams) { + public readonly ClientObjectReference Atom = atom; public readonly ScreenCoordinates InitialMousePos = initialMousePos; public readonly ClickParams ClickParams = clickParams; public bool IsDrag; // If the current click is considered a drag (if the mouse has moved after the click) @@ -73,7 +73,7 @@ public void HandleStatClick(string atomRef, bool isMiddle) { RaiseNetworkEvent(new StatClickedEvent(atomRef, isMiddle, shift, ctrl, alt)); } - private (AtomReference Atom, Vector2i IconPosition)? GetAtomUnderMouse(ScalingViewport viewport, GUIBoundKeyEventArgs args) { + private (ClientObjectReference Atom, Vector2i IconPosition)? GetAtomUnderMouse(ScalingViewport viewport, GUIBoundKeyEventArgs args) { _dreamViewOverlay ??= _overlayManager.GetOverlay(); if(_dreamViewOverlay.MouseMap == null) return null; diff --git a/OpenDreamClient/Interface/Controls/ControlInfo.cs b/OpenDreamClient/Interface/Controls/ControlInfo.cs index 884ee6e261..98d4d4b6b0 100644 --- a/OpenDreamClient/Interface/Controls/ControlInfo.cs +++ b/OpenDreamClient/Interface/Controls/ControlInfo.cs @@ -1,7 +1,9 @@ +using System.Linq; using OpenDreamShared.Network.Messages; using OpenDreamClient.Input; using OpenDreamClient.Interface.Descriptors; using OpenDreamClient.Interface.Html; +using OpenDreamShared.Dream; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; @@ -35,7 +37,7 @@ public StatEntry(ControlInfo owner, IEntitySystemManager entitySystemManager) { _owner = owner; _entitySystemManager = entitySystemManager; - // TODO: Change color when the mouse is hovering + // TODO: Change color when the mouse is hovering (if clickable) // I couldn't find a way to do this without recreating the FormattedMessage ValueLabel.MouseFilter = MouseFilterMode.Stop; ValueLabel.OnKeyBindDown += OnKeyBindDown; @@ -143,11 +145,16 @@ private StatEntry GetEntry(int index) { } internal sealed class VerbPanel : InfoPanel { - [Dependency] private readonly IDreamInterfaceManager _dreamInterface = default!; + public static readonly string DefaultVerbPanel = "Verbs"; // TODO: default_verb_category + + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + private readonly ClientVerbSystem? _verbSystem; + private readonly VerbPanelGrid _grid; public VerbPanel(string name) : base(name) { IoCManager.InjectDependencies(this); + _entitySystemManager.TryGetEntitySystem(out _verbSystem); var scrollContainer = new ScrollContainer { HScrollEnabled = false @@ -161,22 +168,22 @@ public VerbPanel(string name) : base(name) { AddChild(scrollContainer); } - public void RefreshVerbs() { + public void RefreshVerbs(IEnumerable<(int, ClientObjectReference, VerbSystem.VerbInfo)> verbs) { _grid.Children.Clear(); - foreach ((string verbName, string verbId, string verbCategory) in _dreamInterface.AvailableVerbs) { - if (verbCategory != PanelName) + foreach (var (verbId, src, verbInfo) in verbs.Order(VerbNameComparer.OrdinalInstance)) { + if (verbInfo.GetCategoryOrDefault(DefaultVerbPanel) != PanelName) continue; - Button verbButton = new Button() { + Button verbButton = new Button { Margin = new Thickness(2), - Text = verbName, + Text = verbInfo.Name, TextAlign = Label.AlignMode.Center }; verbButton.Label.Margin = new Thickness(6, 0, 6, 2); verbButton.OnPressed += _ => { - _dreamInterface.RunCommand(verbId); + _verbSystem?.ExecuteVerb(src, verbId); }; _grid.Children.Add(verbButton); @@ -194,7 +201,7 @@ public sealed class ControlInfo : InterfaceControl { private readonly Dictionary _statPanels = new(); private readonly SortedDictionary _verbPanels = new(); - private bool _defaultPanelSent = false; + private bool _defaultPanelSent; public ControlInfo(ControlDescriptor controlDescriptor, ControlWindow window) : base(controlDescriptor, window) { IoCManager.InjectDependencies(this); @@ -204,24 +211,35 @@ protected override Control CreateUIElement() { _tabControl = new TabContainer(); _tabControl.OnTabChanged += OnSelectionChanged; - RefreshVerbs(); - _tabControl.OnVisibilityChanged += (args) => { + _tabControl.OnVisibilityChanged += args => { if (args.Visible) { OnShowEvent(); } else { OnHideEvent(); } }; + if(ControlDescriptor.IsVisible) OnShowEvent(); else OnHideEvent(); + return _tabControl; } - public void RefreshVerbs() { + public void RefreshVerbs(ClientVerbSystem verbSystem) { + IEnumerable<(int, ClientObjectReference, VerbSystem.VerbInfo)> verbs = verbSystem.GetExecutableVerbs(); + + foreach (var (_, _, verb) in verbs) { + var category = verb.GetCategoryOrDefault(VerbPanel.DefaultVerbPanel); + + if (!HasVerbPanel(category)) { + CreateVerbPanel(category); + } + } + foreach (var panel in _verbPanels) { - _verbPanels[panel.Key].RefreshVerbs(); + _verbPanels[panel.Key].RefreshVerbs(verbs); } } @@ -310,3 +328,15 @@ public void OnHideEvent() { } } } + +internal sealed class VerbNameComparer(bool ordinal) : IComparer<(int, ClientObjectReference, VerbSystem.VerbInfo)> { + // Verbs are displayed alphabetically with uppercase coming first (BYOND behavior) + public static VerbNameComparer OrdinalInstance = new(true); + + // Verbs are displayed alphabetically according to the user's culture + public static VerbNameComparer CultureInstance = new(false); + + public int Compare((int, ClientObjectReference, VerbSystem.VerbInfo) a, + (int, ClientObjectReference, VerbSystem.VerbInfo) b) => + string.Compare(a.Item3.Name, b.Item3.Name, ordinal ? StringComparison.Ordinal : StringComparison.CurrentCulture); +} diff --git a/OpenDreamClient/Interface/DebugWindows/MacrosWindow.cs b/OpenDreamClient/Interface/DebugWindows/MacrosWindow.cs index e859eb2e41..30ae0739ce 100644 --- a/OpenDreamClient/Interface/DebugWindows/MacrosWindow.cs +++ b/OpenDreamClient/Interface/DebugWindows/MacrosWindow.cs @@ -9,8 +9,6 @@ namespace OpenDreamClient.Interface.DebugWindows; ///
public sealed class MacrosWindow : OSWindow { [Dependency] private readonly IDreamInterfaceManager _interfaceManager = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - public MacrosWindow() { IoCManager.InjectDependencies(this); diff --git a/OpenDreamClient/Interface/DebugWindows/VerbsWindow.cs b/OpenDreamClient/Interface/DebugWindows/VerbsWindow.cs new file mode 100644 index 0000000000..5fe2204d58 --- /dev/null +++ b/OpenDreamClient/Interface/DebugWindows/VerbsWindow.cs @@ -0,0 +1,121 @@ +using System.Linq; +using OpenDreamClient.Interface.Controls; +using OpenDreamShared.Dream; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Console; + +namespace OpenDreamClient.Interface.DebugWindows; + +/// +/// A debug window that displays all existing verbs, and all executable verbs +/// +public sealed class VerbsWindow : OSWindow { + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + public VerbsWindow() { + IoCManager.InjectDependencies(this); + + Title = "Verbs"; + SizeToContent = WindowSizeToContent.WidthAndHeight; + + var verbSystem = _entitySystemManager.GetEntitySystem(); + var tabContainer = new TabContainer { + Children = { CreateAllVerbsTab(verbSystem), CreateExecutableVerbsTab(verbSystem) } + }; + + AddChild(tabContainer); + } + + private ScrollContainer CreateAllVerbsTab(ClientVerbSystem verbSystem) { + var grid = new GridContainer { + Columns = 3 + }; + + foreach (var verbInfo in verbSystem.GetAllVerbs().Order(VerbsWindowNameComparer.Instance)) { + grid.AddChild(new Label { + Text = verbInfo.GetCommandName(), + Margin = new(3) + }); + + grid.AddChild(new Label { + Text = verbInfo.Name, + Margin = new(3) + }); + + grid.AddChild(new Label { + Text = verbInfo.Category, + Margin = new(3) + }); + } + + var scroll = new ScrollContainer { + Children = { grid }, + HScrollEnabled = false, + MinSize = new Vector2(520, 180) + }; + + TabContainer.SetTabTitle(scroll, "All Verbs"); + return scroll; + } + + private ScrollContainer CreateExecutableVerbsTab(ClientVerbSystem verbSystem) { + var grid = new GridContainer { + Columns = 4 + }; + + foreach (var (_, src, verbInfo) in verbSystem.GetExecutableVerbs(true).Order(VerbNameComparer.CultureInstance)) { + grid.AddChild(new Label { + Text = verbInfo.GetCommandName(), + Margin = new(3) + }); + + grid.AddChild(new Label { + Text = verbInfo.Name, + Margin = new(3) + }); + + grid.AddChild(new Label { + Text = verbInfo.Category, + Margin = new(3) + }); + + grid.AddChild(new Label { + Text = src.Type.ToString(), + Margin = new(3) + }); + } + + var scroll = new ScrollContainer { + Children = { grid }, + HScrollEnabled = false, + MinSize = new Vector2(520, 180) + }; + + TabContainer.SetTabTitle(scroll, "Executable Verbs"); + return scroll; + } +} + +public sealed class ShowVerbsCommand : IConsoleCommand { + // ReSharper disable once StringLiteralTypo + public string Command => "showverbs"; + public string Description => "Display the list of existing verbs and list of executable verbs"; + public string Help => ""; + + public void Execute(IConsoleShell shell, string argStr, string[] args) { + if (args.Length != 0) { + shell.WriteError("This command does not take any arguments!"); + return; + } + + new VerbsWindow().Show(); + } +} + +// Verbs are displayed alphabetically +internal sealed class VerbsWindowNameComparer : IComparer { + public static VerbsWindowNameComparer Instance = new(); + + public int Compare(VerbSystem.VerbInfo a, VerbSystem.VerbInfo b) => + string.Compare(a.Name, b.Name, StringComparison.CurrentCulture); +} diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index 303c50986a..c66cd43445 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -53,8 +53,6 @@ internal sealed class DreamInterfaceManager : IDreamInterfaceManager { public ControlInfo? DefaultInfo { get; private set; } public ControlMap? DefaultMap { get; private set; } - public (string, string, string)[] AvailableVerbs { get; private set; } = Array.Empty<(string, string, string)>(); - public Dictionary Windows { get; } = new(); public Dictionary Menus { get; } = new(); public Dictionary MacroSets { get; } = new(); @@ -110,7 +108,6 @@ public void Initialize() { _netManager.RegisterNetMessage(RxUpdateStatPanels); _netManager.RegisterNetMessage(RxSelectStatPanel); - _netManager.RegisterNetMessage(RxUpdateAvailableVerbs); _netManager.RegisterNetMessage(RxOutput); _netManager.RegisterNetMessage(RxAlert); _netManager.RegisterNetMessage(RxPrompt); @@ -136,25 +133,6 @@ private void RxSelectStatPanel(MsgSelectStatPanel message) { DefaultInfo?.SelectStatPanel(message.StatPanel); } - private void RxUpdateAvailableVerbs(MsgUpdateAvailableVerbs message) { - AvailableVerbs = message.AvailableVerbs; - - // Verbs are displayed alphabetically with uppercase coming first - Array.Sort(AvailableVerbs, (a, b) => string.CompareOrdinal(a.Item1, b.Item1)); - - if (DefaultInfo == null) - return; // No verb panel to show these on - - foreach (var verb in AvailableVerbs) { - // Verb category - if (verb.Item3 != string.Empty && !DefaultInfo.HasVerbPanel(verb.Item3)) { - DefaultInfo.CreateVerbPanel(verb.Item3); - } - } - - DefaultInfo.RefreshVerbs(); - } - private void RxOutput(MsgOutput pOutput) { InterfaceControl? interfaceElement; string? data = null; @@ -191,26 +169,11 @@ public void OpenAlert(string title, string message, string button1, string? butt } private void RxPrompt(MsgPrompt pPrompt) { - PromptWindow? prompt = null; - bool canCancel = (pPrompt.Types & DreamValueType.Null) == DreamValueType.Null; - void OnPromptClose(DreamValueType responseType, object? response) { OnPromptFinished(pPrompt.PromptId, responseType, response); } - if ((pPrompt.Types & DreamValueType.Text) == DreamValueType.Text) { - prompt = new TextPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } else if ((pPrompt.Types & DreamValueType.Num) == DreamValueType.Num) { - prompt = new NumberPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } else if ((pPrompt.Types & DreamValueType.Message) == DreamValueType.Message) { - prompt = new MessagePrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } else if ((pPrompt.Types & DreamValueType.Color) == DreamValueType.Color) { - prompt = new ColorPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); - } - - if (prompt != null) { - ShowPrompt(prompt); - } + Prompt(pPrompt.Types, pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, OnPromptClose); } private void RxPromptList(MsgPromptList pPromptList) { @@ -417,14 +380,33 @@ public void SaveScreenshot(bool openDialog) { }); } - public void RunCommand(string command) { - switch (command) { + public void Prompt(DreamValueType types, string title, string message, string defaultValue, Action? onClose) { + PromptWindow? prompt = null; + bool canCancel = (types & DreamValueType.Null) == DreamValueType.Null; + + if ((types & DreamValueType.Text) == DreamValueType.Text) { + prompt = new TextPrompt(title, message, defaultValue, canCancel, onClose); + } else if ((types & DreamValueType.Num) == DreamValueType.Num) { + prompt = new NumberPrompt(title, message, defaultValue, canCancel, onClose); + } else if ((types & DreamValueType.Message) == DreamValueType.Message) { + prompt = new MessagePrompt(title, message, defaultValue, canCancel, onClose); + } else if ((types & DreamValueType.Color) == DreamValueType.Color) { + prompt = new ColorPrompt(title, message, defaultValue, canCancel, onClose); + } + + if (prompt != null) { + ShowPrompt(prompt); + } + } + + public void RunCommand(string fullCommand) { + switch (fullCommand) { case string x when x.StartsWith(".quit"): IoCManager.Resolve().ClientDisconnect(".quit used"); break; case string x when x.StartsWith(".screenshot"): - string[] split = command.Split(" "); + string[] split = fullCommand.Split(" "); SaveScreenshot(split.Length == 1 || split[1] != "auto"); break; @@ -434,7 +416,7 @@ public void RunCommand(string command) { case string x when x.StartsWith(".winset"): // Everything after .winset, excluding the space and quotes - string winsetParams = command.Substring(7); //clip .winset + string winsetParams = fullCommand.Substring(7); //clip .winset winsetParams = winsetParams.Trim(); //clip space winsetParams = winsetParams.Trim('\"'); //clip quotes @@ -442,9 +424,49 @@ public void RunCommand(string command) { break; default: { - // Send the entire command to the server. - // It has more info about argument types so it can parse it better than we can. - _netManager.ClientSendMessage(new MsgCommand() { Command = command }); + // TODO: Arguments are a little more complicated than "split by spaces" + // e.g. strings can be passed + string[] args = fullCommand.Split(' ', StringSplitOptions.TrimEntries); + string command = args[0].ToLowerInvariant(); // Case-insensitive + + if (!_entitySystemManager.TryGetEntitySystem(out ClientVerbSystem? verbSystem)) + return; + var ret = verbSystem.FindVerbWithCommandName(command); + if (ret is not var (verbId, verbSrc, verbInfo)) + return; + + if (args.Length == 1) { // No args given; Let the verb system handle the possible prompting + verbSystem.ExecuteVerb(ClientObjectReference.Client, verbId); + } else { // Attempt to parse the given arguments + if (args.Length != verbInfo.Arguments.Length + 1) { + _sawmill.Error( + $"Attempted to call a verb with {verbInfo.Arguments.Length} argument(s) with only {args.Length - 1}"); + return; + } + + var arguments = new object?[verbInfo.Arguments.Length]; + for (int i = 0; i < verbInfo.Arguments.Length; i++) { + DreamValueType argumentType = verbInfo.Arguments[i].Types; + + if (argumentType == DreamValueType.Text) { + arguments[i] = args[i + 1]; + } else if (argumentType == DreamValueType.Num) { + if (!float.TryParse(args[i + 1], out var numArg)) { + _sawmill.Error( + $"Invalid number argument \"{args[i + 1]}\"; ignoring command ({fullCommand})"); + return; + } + + arguments[i] = numArg; + } else { + _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); + return; + } + } + + verbSystem.ExecuteVerb(ClientObjectReference.Client, verbId, arguments); + } + break; } } @@ -628,7 +650,6 @@ public void WinClone(string controlId, string cloneId) { private void Reset() { _userInterfaceManager.MainViewport.Visible = false; - AvailableVerbs = Array.Empty<(string, string, string)>(); Windows.Clear(); Menus.Clear(); MacroSets.Clear(); @@ -710,7 +731,6 @@ private void OnPromptFinished(int promptId, DreamValueType responseType, object? } public interface IDreamInterfaceManager { - (string, string, string)[] AvailableVerbs { get; } Dictionary Windows { get; } Dictionary Menus { get; } Dictionary MacroSets { get; } @@ -726,7 +746,8 @@ public interface IDreamInterfaceManager { void SaveScreenshot(bool openDialog); void LoadInterfaceFromSource(string source); - void RunCommand(string command); + void Prompt(DreamValueType types, string title, string message, string defaultValue, Action? onClose); + void RunCommand(string fullCommand); void StartRepeatingCommand(string command); void StopRepeatingCommand(string command); void WinSet(string? controlId, string winsetParams); diff --git a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs index 16c167f0dc..ed410a0ae3 100644 --- a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs @@ -44,7 +44,11 @@ public void WinSet(string? controlId, string winsetParams) { } - public void RunCommand(string command) { + public void Prompt(DreamValueType types, string title, string message, string defaultValue, Action? onClose) { + + } + + public void RunCommand(string fullCommand) { } diff --git a/OpenDreamClient/Interface/InterfaceMacro.cs b/OpenDreamClient/Interface/InterfaceMacro.cs index 9ac6b58b9a..661b303bde 100644 --- a/OpenDreamClient/Interface/InterfaceMacro.cs +++ b/OpenDreamClient/Interface/InterfaceMacro.cs @@ -155,11 +155,7 @@ internal struct ParsedKeybind { private static Dictionary? keyToKeyName; public static Key KeyNameToKey(string key) { - if (keyNameToKey.TryGetValue(key, out Key result)) { - return result; - } else { - return Keyboard.Key.Unknown; - } + return keyNameToKey.GetValueOrDefault(key, Keyboard.Key.Unknown); } public static string? KeyToKeyName(Key key) { @@ -170,11 +166,7 @@ public static Key KeyNameToKey(string key) { } } - if (keyToKeyName.TryGetValue(key, out var result)) { - return result; - } else { - return null; - } + return keyToKeyName.GetValueOrDefault(key); } public static ParsedKeybind Parse(string keybind) { diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index 35a8dd3bad..bf4eb18ae0 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -33,8 +33,9 @@ public sealed class AtomManager { private readonly Dictionary _definitionAppearanceCache = new(); private ServerAppearanceSystem AppearanceSystem => _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); + private ServerVerbSystem VerbSystem => _verbSystem ??= _entitySystemManager.GetEntitySystem(); private ServerAppearanceSystem? _appearanceSystem; - private MetaDataSystem? _metaDataSystem; + private ServerVerbSystem? _verbSystem; // ReSharper disable ForCanBeConvertedToForeach (the collections could be added to) public IEnumerable EnumerateAtoms(TreeEntry? filterType = null) { @@ -205,19 +206,6 @@ public void DeleteMovableEntity(DreamObjectMovable movable) { _entityManager.DeleteEntity(movable.Entity); } - public DreamObjectAtom? GetAtom(AtomReference reference) { - switch (reference.AtomType) { - case AtomReference.RefType.Entity: - TryGetMovableFromEntity(_entityManager.GetEntity(reference.Entity), out var atom); - return atom; - case AtomReference.RefType.Turf: - _dreamMapManager.TryGetTurfAt((reference.TurfX, reference.TurfY), reference.TurfZ, out var turf); - return turf; - } - - return null; - } - public bool IsValidAppearanceVar(string name) { switch (name) { case "icon": @@ -239,6 +227,7 @@ public bool IsValidAppearanceVar(string name) { case "render_target": case "transform": case "appearance": + case "verbs": return true; // Get/SetAppearanceVar doesn't handle these @@ -342,6 +331,29 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal : DreamObjectMatrix.IdentityMatrixArray; appearance.Transform = transformArray; + break; + case "verbs": + appearance.Verbs.Clear(); + + if (value.TryGetValueAsDreamList(out var valueList)) { + foreach (DreamValue verbValue in valueList.GetValues()) { + if (!verbValue.TryGetValueAsProc(out var verb)) + continue; + + if (!verb.VerbId.HasValue) + VerbSystem.RegisterVerb(verb); + if (appearance.Verbs.Contains(verb.VerbId!.Value)) + continue; + + appearance.Verbs.Add(verb.VerbId.Value); + } + } else if (value.TryGetValueAsProc(out var verb)) { + if (!verb.VerbId.HasValue) + VerbSystem.RegisterVerb(verb); + + appearance.Verbs.Add(verb.VerbId!.Value); + } + break; case "appearance": throw new Exception("Cannot assign the appearance var on an appearance"); @@ -570,6 +582,14 @@ public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { appearance.Transform = DreamObjectMatrix.MatrixToTransformFloatArray(transformMatrix); } + if (def.Verbs != null) { + foreach (var verb in def.Verbs) { + var verbProc = _objectTree.Procs[verb]; + + appearance.Verbs.Add(verbProc.VerbId!.Value); + } + } + _definitionAppearanceCache.Add(def, appearance); return appearance; } diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 9db17f88a4..9765fdd8db 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -23,8 +23,8 @@ public sealed class DreamConnection { private readonly ServerScreenOverlaySystem? _screenOverlaySystem; private readonly ServerClientImagesSystem? _clientImagesSystem; + private readonly ServerVerbSystem? _verbSystem; - [ViewVariables] private readonly Dictionary _availableVerbs = new(); [ViewVariables] private readonly Dictionary> _statPanels = new(); [ViewVariables] private bool _currentlyUpdatingStat; @@ -58,8 +58,6 @@ public DreamObjectMob? Mob { _mob.Key = Session!.Name; _mob.SpawnProc("Login", usr: _mob); } - - UpdateAvailableVerbs(); } } } @@ -101,16 +99,16 @@ public DreamConnection() { _entitySystemManager.TryGetEntitySystem(out _screenOverlaySystem); _entitySystemManager.TryGetEntitySystem(out _clientImagesSystem); + _entitySystemManager.TryGetEntitySystem(out _verbSystem); } public void HandleConnection(ICommonSession session) { - var client = new DreamObjectClient(_objectTree.Client.ObjectDefinition, this, _screenOverlaySystem, _clientImagesSystem); - Session = session; - Client = client; + Client = new DreamObjectClient(_objectTree.Client.ObjectDefinition, this, _screenOverlaySystem, _clientImagesSystem); Client.InitSpawn(new()); + _verbSystem?.UpdateClientVerbs(Client); SendClientInfoUpdate(); } @@ -134,64 +132,6 @@ public void HandleDisconnection() { Session = null; } - public void UpdateAvailableVerbs() { - _availableVerbs.Clear(); - var verbs = new List<(string, string, string)>(); - - void AddVerbs(DreamObject src, IEnumerable adding) { - foreach (DreamValue mobVerb in adding) { - if (!mobVerb.TryGetValueAsProc(out var proc)) - continue; - - string verbName = proc.VerbName ?? proc.Name; - string verbId = verbName.ToLowerInvariant().Replace(" ", "-"); // Case-insensitive, dashes instead of spaces - if (_availableVerbs.ContainsKey(verbId)) { - // BYOND will actually show the user two verbs with different capitalization/dashes, but they will both execute the same verb. - // We make a warning and ignore the latter ones instead. - _sawmill.Warning($"User \"{Session.Name}\" has multiple verb commands named \"{verbId}\", ignoring all but the first"); - continue; - } - - _availableVerbs.Add(verbId, (src, proc)); - - // Don't send invisible verbs. - if (_mob != null && proc.Invisibility > _mob.SeeInvisible) { - continue; - } - - // Don't send hidden verbs. Names starting with "." count as hidden. - if ((proc.Attributes & ProcAttributes.Hidden) == ProcAttributes.Hidden || - verbName.StartsWith('.')) { - continue; - } - - string? category = proc.VerbCategory; - // Explicitly null category is hidden from verb panels, "" category becomes the default_verb_category - if (category == string.Empty) { - // But if default_verb_category is null, we hide it from the verb panel - Client.GetVariable("default_verb_category").TryGetValueAsString(out category); - } - - // Null category is serialized as an empty string and treated as hidden - verbs.Add((verbName, verbId, category ?? string.Empty)); - } - } - - if (Client != null) { - AddVerbs(Client, Client.Verbs.GetValues()); - } - - if (Mob != null) { - AddVerbs(Mob, Mob.Verbs.GetValues()); - } - - var msg = new MsgUpdateAvailableVerbs() { - AvailableVerbs = verbs.ToArray() - }; - - Session?.ConnectedClient.SendMessage(msg); - } - public void UpdateStat() { if (Session == null || Client == null || _currentlyUpdatingStat) return; @@ -317,9 +257,8 @@ public void OutputControl(string message, string? control) { Session?.ConnectedClient.SendMessage(msg); } + // TODO: Remove this. Vestigial and doesn't run all commands. public void HandleCommand(string fullCommand) { - // TODO: Arguments are a little more complicated than "split by spaces" - // e.g. strings can be passed string[] args = fullCommand.Split(' ', StringSplitOptions.TrimEntries); string command = args[0].ToLowerInvariant(); // Case-insensitive @@ -349,49 +288,6 @@ public void HandleCommand(string fullCommand) { if (Mob != null) _walkManager.StopWalks(Mob); Client?.SpawnProc(movementProc, Mob); break; - - default: { - if (_availableVerbs.TryGetValue(command, out var value)) { - (DreamObject verbSrc, DreamProc verb) = value; - - DreamThread.Run(fullCommand, async (state) => { - DreamValue[] arguments; - if (verb.ArgumentNames != null) { - arguments = new DreamValue[verb.ArgumentNames.Count]; - - // TODO: this should probably be done on the client, shouldn't it? - if (args.Length == 1) { // No args given; prompt the client for them - for (int i = 0; i < verb.ArgumentNames.Count; i++) { - string argumentName = verb.ArgumentNames[i]; - DreamValueType argumentType = verb.ArgumentTypes[i]; - DreamValue argumentValue = await Prompt(argumentType, title: string.Empty, // No settable title for verbs - argumentName, defaultValue: string.Empty); // No default value for verbs - - arguments[i] = argumentValue; - } - } else { // Attempt to parse the given arguments - for (int i = 0; i < verb.ArgumentNames.Count; i++) { - DreamValueType argumentType = verb.ArgumentTypes[i]; - - if (argumentType == DreamValueType.Text) { - arguments[i] = new(args[i + 1]); - } else { - _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); - return DreamValue.Null; - } - } - } - } else { - arguments = Array.Empty(); - } - - await state.Call(verb, verbSrc, Mob, arguments); - return DreamValue.Null; - }); - } - - break; - } } } diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 5cba48bf6e..7077a15e01 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -47,7 +47,6 @@ private void InitializeConnectionManager() { _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; _netManager.RegisterNetMessage(); - _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(RxSelectStatPanel); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 1a610ccf9e..744eb192a3 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -20,6 +20,7 @@ namespace OpenDreamRuntime { public sealed partial class DreamManager { + [Dependency] private readonly AtomManager _atomManager = default!; [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IDreamMapManager _dreamMapManager = default!; @@ -28,6 +29,7 @@ public sealed partial class DreamManager { [Dependency] private readonly ITaskManager _taskManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly DreamObjectTree _objectTree = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IStatusHost _statusHost = default!; [Dependency] private readonly IDependencyCollection _dependencyCollection = default!; @@ -303,6 +305,21 @@ public DreamValue LocateRef(string refString) { return DreamValue.Null; } + public DreamObject? GetFromClientReference(DreamConnection connection, ClientObjectReference reference) { + switch (reference.Type) { + case ClientObjectReference.RefType.Client: + return connection.Client; + case ClientObjectReference.RefType.Entity: + _atomManager.TryGetMovableFromEntity(_entityManager.GetEntity(reference.Entity), out var atom); + return atom; + case ClientObjectReference.RefType.Turf: + _dreamMapManager.TryGetTurfAt((reference.TurfX, reference.TurfY), reference.TurfZ, out var turf); + return turf; + } + + return null; + } + public void HandleException(Exception e, string msg = "", string file = "", int line = 0) { if (string.IsNullOrEmpty(msg)) { // Just print the C# exception if we don't override the message msg = e.Message; diff --git a/OpenDreamRuntime/DreamThread.cs b/OpenDreamRuntime/DreamThread.cs index d6770f6514..d79ed311a0 100644 --- a/OpenDreamRuntime/DreamThread.cs +++ b/OpenDreamRuntime/DreamThread.cs @@ -31,6 +31,7 @@ public abstract class DreamProc { public readonly List? ArgumentTypes; + 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 sbyte Invisibility; @@ -86,7 +87,7 @@ public DreamValue GetField(string field) { } public override string ToString() { - var procElement = (SuperProc == null) ? (IsVerb ? "verb/" : "proc/") : String.Empty; // Has "proc/" only if it's not an override + var procElement = (SuperProc == null) ? (IsVerb ? "verb/" : "proc/") : string.Empty; // Has "proc/" only if it's not an override return $"{OwningType.Path}{(OwningType.Path.EndsWith('/') ? string.Empty : "/")}{procElement}{Name}"; } diff --git a/OpenDreamRuntime/Input/DreamCommandSystem.cs b/OpenDreamRuntime/Input/DreamCommandSystem.cs index 6095c58b1d..f95617a37f 100644 --- a/OpenDreamRuntime/Input/DreamCommandSystem.cs +++ b/OpenDreamRuntime/Input/DreamCommandSystem.cs @@ -13,7 +13,6 @@ internal sealed class DreamCommandSystem : EntitySystem{ private readonly HashSet<(string Command, ICommonSession session)> _repeatingCommands = new(); public override void Initialize() { - _netManager.RegisterNetMessage(OnCommandEvent); _netManager.RegisterNetMessage(OnRepeatCommandEvent); _netManager.RegisterNetMessage(OnStopRepeatCommandEvent); } @@ -24,10 +23,6 @@ public void RunRepeatingCommands() { } } - private void OnCommandEvent(MsgCommand message) { - RunCommand(message.Command, _playerManager.GetSessionByChannel(message.MsgChannel)); - } - private void OnRepeatCommandEvent(MsgCommandRepeatStart message) { var tuple = (message.Command, _playerManager.GetSessionByChannel(message.MsgChannel)); diff --git a/OpenDreamRuntime/Input/MouseInputSystem.cs b/OpenDreamRuntime/Input/MouseInputSystem.cs index 3500141670..a87d15ef2b 100644 --- a/OpenDreamRuntime/Input/MouseInputSystem.cs +++ b/OpenDreamRuntime/Input/MouseInputSystem.cs @@ -1,6 +1,5 @@ using System.Collections.Specialized; using System.Web; -using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamShared.Input; @@ -20,23 +19,25 @@ public override void Initialize() { } private void OnAtomClicked(AtomClickedEvent e, EntitySessionEventArgs sessionEvent) { - var atom = _atomManager.GetAtom(e.Atom); - if (atom == null) + var connection = _dreamManager.GetConnectionBySession(sessionEvent.SenderSession); + var clicked = _dreamManager.GetFromClientReference(connection, e.ClickedAtom); + if (clicked is not DreamObjectAtom atom) return; HandleAtomClick(e, atom, sessionEvent); } private void OnAtomDragged(AtomDraggedEvent e, EntitySessionEventArgs sessionEvent) { - var src = _atomManager.GetAtom(e.SrcAtom); - if (src == null) + var connection = _dreamManager.GetConnectionBySession(sessionEvent.SenderSession); + var src = _dreamManager.GetFromClientReference(connection, e.SrcAtom); + if (src is not DreamObjectAtom srcAtom) return; - var over = (e.OverAtom != null) ? _atomManager.GetAtom(e.OverAtom.Value) : null; - var session = sessionEvent.SenderSession; - var connection = _dreamManager.GetConnectionBySession(session); var usr = connection.Mob; - var srcPos = _atomManager.GetAtomPosition(src); + var srcPos = _atomManager.GetAtomPosition(srcAtom); + var over = (e.OverAtom != null) + ? _dreamManager.GetFromClientReference(connection, e.OverAtom.Value) as DreamObjectAtom + : null; _mapManager.TryGetTurfAt((srcPos.X, srcPos.Y), srcPos.Z, out var srcLoc); @@ -65,7 +66,7 @@ private void OnStatClicked(StatClickedEvent e, EntitySessionEventArgs sessionEve HandleAtomClick(e, dreamObject, sessionEvent); } - private void HandleAtomClick(IAtomMouseEvent e, DreamObject atom, EntitySessionEventArgs sessionEvent) { + private void HandleAtomClick(IAtomMouseEvent e, DreamObjectAtom atom, EntitySessionEventArgs sessionEvent) { var session = sessionEvent.SenderSession; var connection = _dreamManager.GetConnectionBySession(session); var usr = connection.Mob; diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs index 0b41ba1d01..3e49c63f6f 100644 --- a/OpenDreamRuntime/Objects/DreamObject.cs +++ b/OpenDreamRuntime/Objects/DreamObject.cs @@ -38,6 +38,7 @@ public class DreamObject { protected TransformSystem? TransformSystem => ObjectDefinition.TransformSystem; protected PvsOverrideSystem? PvsOverrideSystem => ObjectDefinition.PvsOverrideSystem; protected MetaDataSystem? MetaDataSystem => ObjectDefinition.MetaDataSystem; + protected ServerVerbSystem? VerbSystem => ObjectDefinition.VerbSystem; protected Dictionary? Variables; diff --git a/OpenDreamRuntime/Objects/DreamObjectDefinition.cs b/OpenDreamRuntime/Objects/DreamObjectDefinition.cs index 123fd002d6..49b26136ca 100644 --- a/OpenDreamRuntime/Objects/DreamObjectDefinition.cs +++ b/OpenDreamRuntime/Objects/DreamObjectDefinition.cs @@ -29,6 +29,7 @@ public sealed class DreamObjectDefinition { public readonly TransformSystem? TransformSystem; public readonly PvsOverrideSystem? PvsOverrideSystem; public readonly MetaDataSystem? MetaDataSystem; + public readonly ServerVerbSystem? VerbSystem; public readonly TreeEntry TreeEntry; public string Type => TreeEntry.Path; @@ -72,6 +73,7 @@ public DreamObjectDefinition(DreamObjectDefinition copyFrom) { TransformSystem = copyFrom.TransformSystem; PvsOverrideSystem = copyFrom.PvsOverrideSystem; MetaDataSystem = copyFrom.MetaDataSystem; + VerbSystem = copyFrom.VerbSystem; TreeEntry = copyFrom.TreeEntry; InitializationProc = copyFrom.InitializationProc; @@ -86,7 +88,7 @@ public DreamObjectDefinition(DreamObjectDefinition copyFrom) { Verbs = new List(copyFrom.Verbs); } - public DreamObjectDefinition(DreamManager dreamManager, DreamObjectTree objectTree, AtomManager atomManager, IDreamMapManager dreamMapManager, IMapManager mapManager, DreamResourceManager dreamResourceManager, WalkManager walkManager, IEntityManager entityManager, IPlayerManager playerManager, ISerializationManager serializationManager, ServerAppearanceSystem? appearanceSystem, TransformSystem? transformSystem, PvsOverrideSystem pvsOverrideSystem, MetaDataSystem metaDataSystem, TreeEntry? treeEntry) { + public DreamObjectDefinition(DreamManager dreamManager, DreamObjectTree objectTree, AtomManager atomManager, IDreamMapManager dreamMapManager, IMapManager mapManager, DreamResourceManager dreamResourceManager, WalkManager walkManager, IEntityManager entityManager, IPlayerManager playerManager, ISerializationManager serializationManager, ServerAppearanceSystem? appearanceSystem, TransformSystem? transformSystem, PvsOverrideSystem? pvsOverrideSystem, MetaDataSystem? metaDataSystem, ServerVerbSystem? verbSystem, TreeEntry? treeEntry) { DreamManager = dreamManager; ObjectTree = objectTree; AtomManager = atomManager; @@ -101,6 +103,7 @@ public DreamObjectDefinition(DreamManager dreamManager, DreamObjectTree objectTr TransformSystem = transformSystem; PvsOverrideSystem = pvsOverrideSystem; MetaDataSystem = metaDataSystem; + VerbSystem = verbSystem; TreeEntry = treeEntry; diff --git a/OpenDreamRuntime/Objects/DreamObjectTree.cs b/OpenDreamRuntime/Objects/DreamObjectTree.cs index 6a2ba39e87..dd66697e71 100644 --- a/OpenDreamRuntime/Objects/DreamObjectTree.cs +++ b/OpenDreamRuntime/Objects/DreamObjectTree.cs @@ -64,6 +64,7 @@ public sealed class DreamObjectTree { private TransformSystem? _transformSystem; private PvsOverrideSystem? _pvsOverrideSystem; private MetaDataSystem? _metaDataSystem; + private ServerVerbSystem? _verbSystem; public void LoadJson(DreamCompiledJson json) { var types = json.Types ?? Array.Empty(); @@ -76,6 +77,7 @@ public void LoadJson(DreamCompiledJson json) { _entitySystemManager.TryGetEntitySystem(out _transformSystem); _entitySystemManager.TryGetEntitySystem(out _pvsOverrideSystem); _entitySystemManager.TryGetEntitySystem(out _metaDataSystem); + _entitySystemManager.TryGetEntitySystem(out _verbSystem); Strings = json.Strings ?? new(); @@ -308,7 +310,7 @@ private void LoadTypesFromJson(DreamTypeJson[] types, ProcDefinitionJson[]? proc foreach (TreeEntry type in GetAllDescendants(Root)) { int typeId = type.Id; DreamTypeJson jsonType = types[typeId]; - var definition = new DreamObjectDefinition(_dreamManager, this, _atomManager, _dreamMapManager, _mapManager, _dreamResourceManager, _walkManager, _entityManager, _playerManager, _serializationManager, _appearanceSystem, _transformSystem, _pvsOverrideSystem, _metaDataSystem, type); + var definition = new DreamObjectDefinition(_dreamManager, this, _atomManager, _dreamMapManager, _mapManager, _dreamResourceManager, _walkManager, _entityManager, _playerManager, _serializationManager, _appearanceSystem, _transformSystem, _pvsOverrideSystem, _metaDataSystem, _verbSystem, type); type.ObjectDefinition = definition; type.TreeIndex = treeIndex++; @@ -396,8 +398,14 @@ private void LoadProcsFromJson(ProcDefinitionJson[]? jsonProcs, int[]? jsonGloba if (jsonProcs != null) { Procs.EnsureCapacity(jsonProcs.Length); - foreach (var proc in jsonProcs) { - Procs.Add(LoadProcJson(Procs.Count, proc)); + foreach (var procJson in jsonProcs) { + var proc = LoadProcJson(Procs.Count, procJson); + + if (proc.IsVerb) { + _verbSystem?.RegisterVerb(proc); + } + + Procs.Add(proc); } } diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index 958a001d5d..2e82b15c04 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -506,35 +506,41 @@ public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth } } -// verbs list -// Keeps track of an atom's or client's verbs -public sealed class VerbsList : DreamList { - private readonly List _verbs = new(); +// client.verbs list +// Keeps track of a client's verbs +public sealed class ClientVerbsList : DreamList { + public readonly List Verbs = new(); - public VerbsList(DreamObjectTree objectTree, DreamObject dreamObject) : base(objectTree.List.ObjectDefinition, 0) { - List? verbs = dreamObject.ObjectDefinition.Verbs; + private readonly DreamObjectClient _client; + private readonly ServerVerbSystem? _verbSystem; + + public ClientVerbsList(DreamObjectTree objectTree, ServerVerbSystem? verbSystem, DreamObjectClient client) : base(objectTree.List.ObjectDefinition, 0) { + _client = client; + _verbSystem = verbSystem; + + List? verbs = _client.ObjectDefinition.Verbs; if (verbs == null) return; - _verbs.EnsureCapacity(verbs.Count); + Verbs.EnsureCapacity(verbs.Count); foreach (int verbId in verbs) { - _verbs.Add(objectTree.Procs[verbId]); + Verbs.Add(objectTree.Procs[verbId]); } } public override DreamValue GetValue(DreamValue key) { if (!key.TryGetValueAsInteger(out var index)) throw new Exception($"Invalid index into verbs list: {key}"); - if (index < 1 || index > _verbs.Count) + if (index < 1 || index > Verbs.Count) throw new Exception($"Out of bounds index on verbs list: {index}"); - return new DreamValue(_verbs[index - 1]); + return new DreamValue(Verbs[index - 1]); } public override List GetValues() { - List values = new(_verbs.Count); + List values = new(Verbs.Count); - foreach (DreamProc verb in _verbs) + foreach (DreamProc verb in Verbs) values.Add(new(verb)); return values; @@ -547,21 +553,96 @@ public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth public override void AddValue(DreamValue value) { if (!value.TryGetValueAsProc(out var verb)) throw new Exception($"Cannot add {value} to verbs list"); - if (_verbs.Contains(verb)) + if (Verbs.Contains(verb)) return; // Even += won't add the verb if it's already in this list - _verbs.Add(verb); + Verbs.Add(verb); + _verbSystem?.RegisterVerb(verb); + _verbSystem?.UpdateClientVerbs(_client); } public override void Cut(int start = 1, int end = 0) { - int verbCount = _verbs.Count + 1; + int verbCount = Verbs.Count + 1; if (end == 0 || end > verbCount) end = verbCount; - _verbs.RemoveRange(start - 1, end - start); + Verbs.RemoveRange(start - 1, end - start); + _verbSystem?.UpdateClientVerbs(_client); } public override int GetLength() { - return _verbs.Count; + return Verbs.Count; + } +} + +// atom's verbs list +// Keeps track of an appearance's verbs (atom.verbs, mutable_appearance.verbs, etc) +public sealed class VerbsList(DreamObjectTree objectTree, AtomManager atomManager, ServerVerbSystem? verbSystem, DreamObjectAtom atom) : DreamList(objectTree.List.ObjectDefinition, 0) { + public override DreamValue GetValue(DreamValue key) { + if (verbSystem == null) + return DreamValue.Null; + if (!key.TryGetValueAsInteger(out var index)) + throw new Exception($"Invalid index into verbs list: {key}"); + + var verbs = GetVerbs(); + if (index < 1 || index > verbs.Count) + throw new Exception($"Out of bounds index on verbs list: {index}"); + + return new DreamValue(verbSystem.GetVerb(verbs[index - 1])); + } + + public override List GetValues() { + var appearance = atomManager.MustGetAppearance(atom); + if (appearance == null || verbSystem == null) + return new List(); + + var values = new List(appearance.Verbs.Count); + + foreach (var verbId in appearance.Verbs) { + var verb = verbSystem.GetVerb(verbId); + + values.Add(new(verb)); + } + + return values; + } + + public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth = false) { + throw new Exception("Cannot set the values of a verbs list"); + } + + public override void AddValue(DreamValue value) { + if (!value.TryGetValueAsProc(out var verb)) + throw new Exception($"Cannot add {value} to verbs list"); + + atomManager.UpdateAppearance(atom, appearance => { + if (!verb.VerbId.HasValue) + verbSystem?.RegisterVerb(verb); + if (!verb.VerbId.HasValue || appearance.Verbs.Contains(verb.VerbId.Value)) + return; // Even += won't add the verb if it's already in this list + + appearance.Verbs.Add(verb.VerbId.Value); + }); + } + + public override void Cut(int start = 1, int end = 0) { + atomManager.UpdateAppearance(atom, appearance => { + int count = appearance.Verbs.Count + 1; + if (end == 0 || end > count) end = count; + + appearance.Verbs.RemoveRange(start - 1, end - start); + }); + } + + public override int GetLength() { + return GetVerbs().Count; + } + + private List GetVerbs() { + IconAppearance? appearance = atomManager.MustGetAppearance(atom); + if (appearance == null) + throw new Exception("Atom has no appearance"); + + return appearance.Verbs; } } @@ -693,7 +774,7 @@ private IconAppearance GetAppearance() { // Operates on an atom's appearance public sealed class DreamVisContentsList : DreamList { [Dependency] private readonly AtomManager _atomManager = default!; - [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; private readonly PvsOverrideSystem? _pvsOverrideSystem; private readonly List _visContents = new(); @@ -753,7 +834,7 @@ public override void AddValue(DreamValue value) { // TODO: Only override the entity's visibility if its parent atom is visible if (entity != EntityUid.Invalid) - _pvsOverrideSystem?.AddGlobalOverride(_entityManager.GetNetEntity(entity)); + _pvsOverrideSystem?.AddGlobalOverride(entity); _atomManager.UpdateAppearance(_atom, appearance => { // Add even an invalid UID to keep this and _visContents in sync diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index a5f413278f..7558450c69 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -10,7 +10,6 @@ public class DreamObjectAtom : DreamObject { public readonly DreamOverlaysList Overlays; public readonly DreamOverlaysList Underlays; public readonly DreamVisContentsList VisContents; - public readonly VerbsList Verbs; public readonly DreamFilterList Filters; public DreamList? VisLocs; // TODO: Implement @@ -18,7 +17,6 @@ public DreamObjectAtom(DreamObjectDefinition objectDefinition) : base(objectDefi Overlays = new(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, false); Underlays = new(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, true); VisContents = new(ObjectTree.List.ObjectDefinition, PvsOverrideSystem, this); - Verbs = new(ObjectTree, this); Filters = new(ObjectTree.List.ObjectDefinition, this); AtomManager.AddAtom(this); @@ -65,7 +63,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { value = new(Underlays); return true; case "verbs": - value = new(Verbs); + value = new(new VerbsList(ObjectTree, AtomManager, VerbSystem, this)); return true; case "filters": value = new(Filters); @@ -156,19 +154,6 @@ protected override void SetVar(string varName, DreamValue value) { break; } - case "verbs": { - Verbs.Cut(); - - if (value.TryGetValueAsDreamList(out var valueList)) { - foreach (DreamValue verbValue in valueList.GetValues()) { - Verbs.AddValue(verbValue); - } - } else if (!value.IsNull) { - Verbs.AddValue(value); - } - - break; - } case "filters": { Filters.Cut(); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs index 1c40cf9bbc..a92e0c3c07 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs @@ -10,13 +10,13 @@ public sealed class DreamObjectClient : DreamObject { public readonly DreamConnection Connection; public readonly ClientScreenList Screen; public readonly ClientImagesList Images; - public readonly VerbsList Verbs; + public readonly ClientVerbsList ClientVerbs; public ViewRange View { get; private set; } public DreamObjectClient(DreamObjectDefinition objectDefinition, DreamConnection connection, ServerScreenOverlaySystem? screenOverlaySystem, ServerClientImagesSystem? clientImagesSystem) : base(objectDefinition) { Connection = connection; Screen = new(ObjectTree, screenOverlaySystem, Connection); - Verbs = new(ObjectTree, this); + ClientVerbs = new(ObjectTree, VerbSystem, this); Images = new(ObjectTree, clientImagesSystem, Connection); DreamManager.Clients.Add(this); @@ -86,7 +86,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { value = new(Screen); return true; case "verbs": - value = new(Verbs); + value = new(ClientVerbs); return true; case "images": value = new(Images); @@ -154,6 +154,19 @@ protected override void SetVar(string varName, DreamValue value) { break; } + case "verbs": { + ClientVerbs.Cut(); + + if (value.TryGetValueAsDreamList(out var valueList)) { + foreach (DreamValue verbValue in valueList.GetValues()) { + ClientVerbs.AddValue(verbValue); + } + } else { + ClientVerbs.AddValue(value); + } + + break; + } case "statpanel": if (!value.TryGetValueAsString(out var statPanel)) return; diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs index 1b82ddc140..3ceb6e995a 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs @@ -21,8 +21,6 @@ public void SetTurfType(DreamObjectDefinition objectDefinition) { ObjectDefinition = objectDefinition; Variables?.Clear(); - Verbs.Cut(); - Initialize(new()); } diff --git a/OpenDreamRuntime/ServerVerbSystem.cs b/OpenDreamRuntime/ServerVerbSystem.cs new file mode 100644 index 0000000000..27278257b8 --- /dev/null +++ b/OpenDreamRuntime/ServerVerbSystem.cs @@ -0,0 +1,147 @@ +using System.Diagnostics.CodeAnalysis; +using DMCompiler.DM; +using OpenDreamRuntime.Objects; +using OpenDreamRuntime.Objects.Types; +using OpenDreamShared.Dream; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Player; + +namespace OpenDreamRuntime; + +public sealed class ServerVerbSystem : VerbSystem { + [Dependency] private readonly DreamManager _dreamManager = default!; + [Dependency] private readonly AtomManager _atomManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + private readonly List _verbs = new(); + private readonly Dictionary _verbIdToProc = new(); + + public override void Initialize() { + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; + + SubscribeNetworkEvent(OnVerbExecuted); + } + + /// + /// Add a verb to the total list of verbs and ensure every client has knowledge of it + /// + /// The verb to register + public void RegisterVerb(DreamProc verb) { + if (verb.VerbId != null) // Verb has already been registered + return; + + var verbInfo = new VerbInfo { + Name = verb.VerbName, + + // TODO: default_verb_category + // Explicitly null category is hidden from verb panels, "" category becomes the default_verb_category + // But if default_verb_category is null, we hide it from the verb panel + Category = verb.VerbCategory ?? string.Empty, + + Invisibility = verb.Invisibility, + HiddenAttribute = (verb.Attributes & ProcAttributes.Hidden) == ProcAttributes.Hidden + }; + + 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); + + RaiseNetworkEvent(new RegisterVerbEvent(verb.VerbId.Value, verbInfo)); + } + + 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 + /// + /// The client to update + public void UpdateClientVerbs(DreamObjectClient client) { + var verbs = client.ClientVerbs.Verbs; + var verbIds = new List(verbs.Count); + + foreach (var verb in verbs) { + if (verb.VerbId == null) + RegisterVerb(verb); + + verbIds.Add(verb.VerbId!.Value); + } + + RaiseNetworkEvent(new UpdateClientVerbsEvent(verbIds), client.Connection.Session!); + } + + private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { + if (e.NewStatus != SessionStatus.InGame) + return; + + // Send the new player a list of every verb + RaiseNetworkEvent(new AllVerbsEvent(_verbs), e.Session); + } + + 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)) + return; + if (msg.Arguments.Length != verb.ArgumentTypes?.Count) + 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] + "'") + }; + } + + DreamThread.Run($"Execute {msg.VerbId} by {connection.Session!.Name}", async state => { + await state.Call(verb, src, connection.Mob, arguments); + return DreamValue.Null; + }); + } + + /// + /// Verifies a user is allowed to execute a verb on a given target + /// + /// The user + /// The target of the verb + /// The verb trying to be executed + /// True if the user is allowed to execute the verb in this way + private bool CanExecute(DreamConnection connection, DreamObject src, DreamProc verb) { + 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; + } else if (src is DreamObjectAtom atom) { + var appearance = _atomManager.MustGetAppearance(atom); + + if (appearance?.Verbs.Contains(verb.VerbId.Value) is not true) // Inside atom.verbs? + return false; + } + + // TODO: Does "set src = ..." allow execution here? + return true; + } +} diff --git a/OpenDreamShared/Dream/AtomReference.cs b/OpenDreamShared/Dream/ClientObjectReference.cs similarity index 60% rename from OpenDreamShared/Dream/AtomReference.cs rename to OpenDreamShared/Dream/ClientObjectReference.cs index 624b65650d..3f38d74856 100644 --- a/OpenDreamShared/Dream/AtomReference.cs +++ b/OpenDreamShared/Dream/ClientObjectReference.cs @@ -6,28 +6,31 @@ namespace OpenDreamShared.Dream; /// -/// Used by the client to refer to something that could be either a turf or an entity +/// Used by the client to refer to something that could be either its own client, a turf, or an entity /// /// This should only be used on the client or when communicating with the client [Serializable, NetSerializable] -public struct AtomReference { +public struct ClientObjectReference { public enum RefType { + Client, Turf, Entity } - public RefType AtomType; + public static readonly ClientObjectReference Client = new() { Type = RefType.Client }; + + public RefType Type; public NetEntity Entity; public int TurfX, TurfY, TurfZ; - public AtomReference(Vector2i turfPos, int z) { - AtomType = RefType.Turf; + public ClientObjectReference(Vector2i turfPos, int z) { + Type = RefType.Turf; (TurfX, TurfY) = turfPos; TurfZ = z; } - public AtomReference(NetEntity entity) { - AtomType = RefType.Entity; + public ClientObjectReference(NetEntity entity) { + Type = RefType.Entity; Entity = entity; } } diff --git a/OpenDreamShared/Dream/IconAppearance.cs b/OpenDreamShared/Dream/IconAppearance.cs index 7847f6d742..7530e1ecff 100644 --- a/OpenDreamShared/Dream/IconAppearance.cs +++ b/OpenDreamShared/Dream/IconAppearance.cs @@ -44,10 +44,13 @@ public sealed class IconAppearance : IEquatable { [ViewVariables] public List Underlays = new(); [ViewVariables] public List VisContents = new(); [ViewVariables] public List Filters = new(); + [ViewVariables] public List Verbs = new(); /// The Transform property of this appearance, in [a,d,b,e,c,f] order - [ViewVariables] public float[] Transform = new float[6] { 1, 0, // a d - 0, 1, // b e - 0, 0 }; // c f + [ViewVariables] public float[] Transform = [ + 1, 0, // a d + 0, 1, // b e + 0, 0 // c f + ]; public IconAppearance() { } @@ -70,10 +73,11 @@ public IconAppearance(IconAppearance appearance) { Invisibility = appearance.Invisibility; Opacity = appearance.Opacity; MouseOpacity = appearance.MouseOpacity; - Overlays = new List(appearance.Overlays); - Underlays = new List(appearance.Underlays); - VisContents = new List(appearance.VisContents); - Filters = new List(appearance.Filters); + Overlays = new(appearance.Overlays); + Underlays = new(appearance.Underlays); + VisContents = new(appearance.VisContents); + Filters = new(appearance.Filters); + Verbs = new(appearance.Verbs); Override = appearance.Override; for (int i = 0; i < 6; i++) { @@ -108,6 +112,7 @@ public bool Equals(IconAppearance? appearance) { if (appearance.Underlays.Count != Underlays.Count) return false; if (appearance.VisContents.Count != VisContents.Count) return false; if (appearance.Filters.Count != Filters.Count) return false; + if (appearance.Verbs.Count != Verbs.Count) return false; if (appearance.Override != Override) return false; for (int i = 0; i < Filters.Count; i++) { @@ -126,6 +131,10 @@ public bool Equals(IconAppearance? appearance) { if (appearance.VisContents[i] != VisContents[i]) return false; } + for (int i = 0; i < Verbs.Count; i++) { + if (appearance.Verbs[i] != Verbs[i]) return false; + } + for (int i = 0; i < 6; i++) { if (!appearance.Transform[i].Equals(Transform[i])) return false; } @@ -199,6 +208,10 @@ public override int GetHashCode() { hashCode.Add(filter); } + foreach (int verb in Verbs) { + hashCode.Add(verb); + } + for (int i = 0; i < 6; i++) { hashCode.Add(Transform[i]); } diff --git a/OpenDreamShared/Dream/VerbSystem.cs b/OpenDreamShared/Dream/VerbSystem.cs new file mode 100644 index 0000000000..9063791c3b --- /dev/null +++ b/OpenDreamShared/Dream/VerbSystem.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace OpenDreamShared.Dream; + +[Virtual] +public class VerbSystem : EntitySystem { + [Serializable, NetSerializable] + public struct VerbInfo { + /// + /// Display name of the verb + /// /mob/verb/show_inventory() becomes "show inventory" + /// or set name = "Show Inventory" + /// + public string Name; + + /// + /// The verb panel this verb shows in + /// set category = "Debug" + /// + public string Category; + + /// + /// The "invisibility" attribute of this verb + /// set invisibility = 50 + /// + public sbyte Invisibility; + + /// + /// The "hidden" attribute of this verb + /// set hidden = TRUE + /// + public bool HiddenAttribute; + + /// + /// The arguments of this verb + /// + public VerbArg[] Arguments; + + /// The text used to execute this verb in an INPUT control + [Pure] + public string GetCommandName() => + Name.ToLowerInvariant().Replace(" ", "-"); // Case-insensitive, dashes instead of spaces + + 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? + public bool IsHidden(bool ignoreHiddenAttr, sbyte seeInvisibility) => + (!ignoreHiddenAttr && HiddenAttribute) || Name.StartsWith('.') || seeInvisibility < Invisibility; + + public override string ToString() => GetCommandName(); + } + + [Serializable, NetSerializable] + public struct VerbArg { + /// + /// Name of the argument + /// + public string Name; + + /// + /// Types the argument is allowed to be + /// + public DreamValueType Types; + } + + [Serializable, NetSerializable] + public sealed class AllVerbsEvent(List verbs) : EntityEventArgs { + public List Verbs = verbs; + } + + [Serializable, NetSerializable] + public sealed class RegisterVerbEvent(int verbId, VerbInfo verbInfo) : EntityEventArgs { + public int VerbId = verbId; + public VerbInfo VerbInfo = verbInfo; + } + + [Serializable, NetSerializable] + public sealed class UpdateClientVerbsEvent(List verbIds) : EntityEventArgs { + public List VerbIds = verbIds; + } + + [Serializable, NetSerializable] + public sealed class ExecuteVerbEvent(ClientObjectReference src, int verbId, object?[] arguments) : EntityEventArgs { + public ClientObjectReference Src = src; + public int VerbId = verbId; + public object?[] Arguments = arguments; + } +} diff --git a/OpenDreamShared/Input/SharedMouseInputSystem.cs b/OpenDreamShared/Input/SharedMouseInputSystem.cs index 0c7a78de12..bb9c73f3f5 100644 --- a/OpenDreamShared/Input/SharedMouseInputSystem.cs +++ b/OpenDreamShared/Input/SharedMouseInputSystem.cs @@ -24,22 +24,22 @@ public struct ClickParams(ScreenLocation screenLoc, bool middle, bool shift, boo } [Serializable, NetSerializable] - public sealed class AtomClickedEvent(AtomReference atom, ClickParams clickParams) : EntityEventArgs, IAtomMouseEvent { - public AtomReference Atom { get; } = atom; + public sealed class AtomClickedEvent(ClientObjectReference clickedAtom, ClickParams clickParams) : EntityEventArgs, IAtomMouseEvent { + public ClientObjectReference ClickedAtom { get; } = clickedAtom; public ClickParams Params { get; } = clickParams; } [Serializable, NetSerializable] - public sealed class AtomDraggedEvent(AtomReference src, AtomReference? over, ClickParams clickParams) : EntityEventArgs, IAtomMouseEvent { - public AtomReference SrcAtom { get; } = src; - public AtomReference? OverAtom { get; } = over; + public sealed class AtomDraggedEvent(ClientObjectReference srcAtom, ClientObjectReference? over, ClickParams clickParams) : EntityEventArgs, IAtomMouseEvent { + public ClientObjectReference SrcAtom { get; } = srcAtom; + public ClientObjectReference? OverAtom { get; } = over; public ClickParams Params { get; } = clickParams; } [Serializable, NetSerializable] public sealed class StatClickedEvent(string atomRef, bool middle, bool shift, bool ctrl, bool alt) : EntityEventArgs, IAtomMouseEvent { - public string AtomRef = atomRef; + public string AtomRef = atomRef; // TODO: Use ClientObjectReference // TODO: icon-x and icon-y // TODO: ScreenLoc doesn't appear at all in the click params diff --git a/OpenDreamShared/Network/Messages/MsgCommand.cs b/OpenDreamShared/Network/Messages/MsgCommand.cs index 045afd676d..9950b25e14 100644 --- a/OpenDreamShared/Network/Messages/MsgCommand.cs +++ b/OpenDreamShared/Network/Messages/MsgCommand.cs @@ -1,27 +1,12 @@ -using System; using Lidgren.Network; using Robust.Shared.Network; using Robust.Shared.Serialization; namespace OpenDreamShared.Network.Messages { - public sealed class MsgCommand : NetMessage { - public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - - public string Command = String.Empty; - - public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - Command = buffer.ReadString(); - } - - public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.Write(Command); - } - } - public sealed class MsgCommandRepeatStart : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - public string Command = String.Empty; + public string Command = string.Empty; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { Command = buffer.ReadString(); @@ -35,7 +20,7 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer public sealed class MsgCommandRepeatStop : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - public string Command = String.Empty; + public string Command = string.Empty; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { Command = buffer.ReadString(); diff --git a/OpenDreamShared/Network/Messages/MsgUpdateAvailableVerbs.cs b/OpenDreamShared/Network/Messages/MsgUpdateAvailableVerbs.cs deleted file mode 100644 index f7985418c2..0000000000 --- a/OpenDreamShared/Network/Messages/MsgUpdateAvailableVerbs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Lidgren.Network; -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace OpenDreamShared.Network.Messages { - public sealed class MsgUpdateAvailableVerbs : NetMessage { - public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - - public (string Name, string Id, string Category)[] AvailableVerbs = Array.Empty<(string, string, string)>(); - - public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - var count = buffer.ReadVariableInt32(); - (string, string, string)[] verbs = new (string, string, string)[count]; - - for (var i = 0; i < count; i++) { - verbs[i] = (buffer.ReadString(), buffer.ReadString(), buffer.ReadString()); - } - - AvailableVerbs = verbs; - } - - public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.WriteVariableInt32(AvailableVerbs.Length); - - foreach (var verb in AvailableVerbs) { - buffer.Write(verb.Item1); - buffer.Write(verb.Item2); - buffer.Write(verb.Item3); - } - } - } -} diff --git a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs index 2182b484df..8807f0b6b2 100644 --- a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs +++ b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs @@ -4,39 +4,25 @@ using System; using System.Collections.Generic; -namespace OpenDreamShared.Rendering { - public abstract class SharedAppearanceSystem : EntitySystem { - [Serializable, NetSerializable] - public sealed class AllAppearancesEvent : EntityEventArgs { - public Dictionary Appearances = new(); +namespace OpenDreamShared.Rendering; - public AllAppearancesEvent(Dictionary appearances) { - Appearances = appearances; - } - } - - [Serializable, NetSerializable] - public sealed class NewAppearanceEvent : EntityEventArgs { - public int AppearanceId { get; } - public IconAppearance Appearance { get; } - - public NewAppearanceEvent(int appearanceID, IconAppearance appearance) { - AppearanceId = appearanceID; - Appearance = appearance; - } - } +public abstract class SharedAppearanceSystem : EntitySystem { + [Serializable, NetSerializable] + public sealed class AllAppearancesEvent(Dictionary appearances) : EntityEventArgs { + public Dictionary Appearances = appearances; + } - [Serializable, NetSerializable] - public sealed class AnimationEvent : EntityEventArgs { - public NetEntity Entity; - public int TargetAppearanceId; - public TimeSpan Duration; + [Serializable, NetSerializable] + public sealed class NewAppearanceEvent(int appearanceId, IconAppearance appearance) : EntityEventArgs { + public int AppearanceId { get; } = appearanceId; + public IconAppearance Appearance { get; } = appearance; + } - public AnimationEvent(NetEntity entity, int targetAppearanceId, TimeSpan duration) { - Entity = entity; - TargetAppearanceId = targetAppearanceId; - Duration = duration; - } - } + [Serializable, NetSerializable] + public sealed class AnimationEvent(NetEntity entity, int targetAppearanceId, TimeSpan duration) + : EntityEventArgs { + public NetEntity Entity = entity; + public int TargetAppearanceId = targetAppearanceId; + public TimeSpan Duration = duration; } } From 5f303b0b5f4a594df9500cfc196bad0bc697d693 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Mon, 29 Jan 2024 06:13:10 +0000 Subject: [PATCH 61/64] default pick weight (#1643) --- OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 08e096cf73..846ace9fab 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1930,8 +1930,7 @@ public static ProcStatus PickWeighted(DMProcState state) { DreamValue value = state.Pop(); if (!state.Pop().TryGetValueAsFloat(out var weight)) { - // Breaking change, no clue what weight BYOND is giving to non-nums - throw new Exception($"pick() weight '{weight}' is not a number"); + weight = 100; } totalWeight += weight; From 0c181159717ab445096604429d8b2717cda4ef2a Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Mon, 29 Jan 2024 06:14:15 +0000 Subject: [PATCH 62/64] Fix isnull (#1642) --- Content.Tests/DMProject/Tests/Builtins/isnull.dm | 6 ++++++ OpenDreamRuntime/DreamValue.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Content.Tests/DMProject/Tests/Builtins/isnull.dm diff --git a/Content.Tests/DMProject/Tests/Builtins/isnull.dm b/Content.Tests/DMProject/Tests/Builtins/isnull.dm new file mode 100644 index 0000000000..e46fed819a --- /dev/null +++ b/Content.Tests/DMProject/Tests/Builtins/isnull.dm @@ -0,0 +1,6 @@ +/proc/RunTest() + ASSERT(isnull(null)) + var/obj/O = new() + ASSERT(!isnull(O)) + del(O) + ASSERT(isnull(O)) \ No newline at end of file diff --git a/OpenDreamRuntime/DreamValue.cs b/OpenDreamRuntime/DreamValue.cs index 43ca2e9b03..23a18b9729 100644 --- a/OpenDreamRuntime/DreamValue.cs +++ b/OpenDreamRuntime/DreamValue.cs @@ -108,7 +108,7 @@ public DreamValue(IconAppearance appearance) { public bool IsNull { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Type == DreamValueType.DreamObject && _refValue == null; + get => Type == DreamValueType.DreamObject && (_refValue == null || Unsafe.As(_refValue).Deleted); } private DreamValue(DreamValueType type, object refValue) { From 143a44c6613772adc205b8a9c827721455b537f6 Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Tue, 30 Jan 2024 02:23:51 +0000 Subject: [PATCH 63/64] Recursive constant list (#1641) Co-authored-by: wixoa --- DMCompiler/Compiler/DM/DMAST.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/DMCompiler/Compiler/DM/DMAST.cs b/DMCompiler/Compiler/DM/DMAST.cs index 971cd63230..faf4914cf9 100644 --- a/DMCompiler/Compiler/DM/DMAST.cs +++ b/DMCompiler/Compiler/DM/DMAST.cs @@ -1324,10 +1324,17 @@ public override void Visit(DMASTVisitor visitor) { } public bool AllValuesConstant() { - return Values.All(value => value is { - Key: DMASTExpressionConstant, - Value: DMASTExpressionConstant - }); + return Values.All( + value => (value is { + Key: DMASTExpressionConstant, + Value: DMASTExpressionConstant + }) + || + (value is { + Key: DMASTExpressionConstant, + Value: DMASTList valueList + } && valueList.AllValuesConstant()) + ); } } From 1f04fdd2c22c2a9b3e40995262a13aaf13c25f3d Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Fri, 2 Feb 2024 20:21:03 +0000 Subject: [PATCH 64/64] Fix hidden verb logic (#1647) --- OpenDreamShared/Dream/VerbSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamShared/Dream/VerbSystem.cs b/OpenDreamShared/Dream/VerbSystem.cs index 9063791c3b..300feb84be 100644 --- a/OpenDreamShared/Dream/VerbSystem.cs +++ b/OpenDreamShared/Dream/VerbSystem.cs @@ -51,7 +51,7 @@ public string GetCategoryOrDefault(string defaultCategory) => // TODO: Hidden verbs probably shouldn't be sent to the client in the first place? public bool IsHidden(bool ignoreHiddenAttr, sbyte seeInvisibility) => - (!ignoreHiddenAttr && HiddenAttribute) || Name.StartsWith('.') || seeInvisibility < Invisibility; + (!ignoreHiddenAttr && (HiddenAttribute || Name.StartsWith('.'))) || seeInvisibility < Invisibility; public override string ToString() => GetCommandName(); }