diff --git a/.github/workflows/compiler-test.yml b/.github/workflows/compiler-test.yml index 9d44fc0634..face818642 100644 --- a/.github/workflows/compiler-test.yml +++ b/.github/workflows/compiler-test.yml @@ -75,7 +75,7 @@ jobs: ref: dev path: nebula - name: Compile Nebula Dev - run: main\bin\DMCompiler\DMCompiler.exe nebula\nebula.dme --suppress-unimplemented + run: main\bin\DMCompiler\DMCompiler.exe nebula\nebula.dme --suppress-unimplemented --version=516.1655 - name: Checkout /vg/station Master uses: actions/checkout@v2 with: @@ -83,7 +83,7 @@ jobs: ref: Bleeding-Edge path: vg - name: Compile /vg/station Master - run: main\bin\DMCompiler\DMCompiler.exe vg\vgstation13.dme --suppress-unimplemented + run: main\bin\DMCompiler\DMCompiler.exe vg\vgstation13.dme --suppress-unimplemented --version=516.1655 - name: Checkout CM Master uses: actions/checkout@v2 with: @@ -99,4 +99,4 @@ jobs: ref: master path: aurora - name: Compile Aurora Master - run: main\bin\DMCompiler\DMCompiler.exe aurora\aurorastation.dme --suppress-unimplemented + run: main\bin\DMCompiler\DMCompiler.exe aurora\aurorastation.dme --suppress-unimplemented --version=516.1655 diff --git a/Content.Tests/DMProject/Tests/Builtins/astype.dm b/Content.Tests/DMProject/Tests/Builtins/astype.dm new file mode 100644 index 0000000000..0a41b553ff --- /dev/null +++ b/Content.Tests/DMProject/Tests/Builtins/astype.dm @@ -0,0 +1,18 @@ + +/datum/foo +/datum/foo/bar + +/proc/test_null() + var/datum/D = new + var/datum/foo/bar/B = astype(D) + return isnull(B) + +/proc/test_type() + var/datum/foo/bar/B = new + var/datum/D = astype(B) + var/datum/foo/F = astype(D) + return F.type + +/proc/RunTest() + ASSERT(test_null()) + ASSERT(test_type() == /datum/foo/bar) diff --git a/Content.Tests/DMProject/Tests/Builtins/sign.dm b/Content.Tests/DMProject/Tests/Builtins/sign.dm new file mode 100644 index 0000000000..26786f754d --- /dev/null +++ b/Content.Tests/DMProject/Tests/Builtins/sign.dm @@ -0,0 +1,10 @@ + +/proc/RunTest() + ASSERT(sign(5.2) == 1) + ASSERT(sign(-5.2) == -1) + ASSERT(sign(0) == 0) + ASSERT(sign(null) == 0) + ASSERT(sign("") == 0) + ASSERT(sign("foo") == 0) + ASSERT(sign(list(1)) == 0) + \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/List/ListSplice.dm b/Content.Tests/DMProject/Tests/List/ListSplice.dm new file mode 100644 index 0000000000..9839d62ad9 --- /dev/null +++ b/Content.Tests/DMProject/Tests/List/ListSplice.dm @@ -0,0 +1,10 @@ + +/proc/RunTest() + var/list/L = list("foo", "bar", "test", "value") + L.Splice(2, 4, "lorem", "ipsum", "word", "another word") + ASSERT(L ~= list("foo","lorem","ipsum","word","another word","value")) + + // Again with list() as the arg + var/list/L2 = list("foo", "bar", "test", "value") + L2.Splice(2, 4, list("lorem", "ipsum", "word", "another word")) + ASSERT(L2 ~= list("foo","lorem","ipsum","word","another word","value")) diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index d583165388..00852ad0bb 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -140,7 +140,8 @@ public enum DreamProcOpcode : byte { Ftp = 0x46, [OpcodeMetadata(-1)] Initial = 0x47, - //0x48 + [OpcodeMetadata(-1)] + AsType = 0x48, [OpcodeMetadata(-1)] IsType = 0x49, [OpcodeMetadata(-2)] diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs index aa024889f6..00d97cbbd1 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs @@ -48,6 +48,7 @@ public sealed class DMASTPower(Location location, DMASTExpression a, DMASTExpres public sealed class DMASTAdd(Location location, DMASTExpression a, DMASTExpression b) : DMASTBinary(location, a, b); public sealed class DMASTSubtract(Location location, DMASTExpression a, DMASTExpression b) : DMASTBinary(location, a, b); public sealed class DMASTArctan2(Location location, DMASTExpression xExpression, DMASTExpression yExpression) : DMASTBinary(location, xExpression, yExpression); +public sealed class DMASTAsType(Location location, DMASTExpression value, DMASTExpression type) : DMASTBinary(location, value, type); public sealed class DMASTIsType(Location location, DMASTExpression value, DMASTExpression type) : DMASTBinary(location, value, type); public sealed class DMASTGetStep(Location location, DMASTExpression refValue, DMASTExpression dir) : DMASTBinary(location, refValue, dir); public sealed class DMASTGetDir(Location location, DMASTExpression loc1, DMASTExpression loc2) : DMASTBinary(location, loc1, loc2); diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs index eee1db30da..aee10c3427 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs @@ -31,6 +31,7 @@ public sealed class DMASTNameof(Location location, DMASTExpression expression) : public sealed class DMASTIsSaved(Location location, DMASTExpression expression) : DMASTUnary(location, expression); public sealed class DMASTIsNull(Location location, DMASTExpression value) : DMASTUnary(location, value); public sealed class DMASTLength(Location location, DMASTExpression value) : DMASTUnary(location, value); +public sealed class DMASTImplicitAsType(Location location, DMASTExpression value) : DMASTUnary(location, value); public sealed class DMASTImplicitIsType(Location location, DMASTExpression value) : DMASTUnary(location, value); /// diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index c0ecacbba4..ec01b396ae 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -2641,6 +2641,16 @@ private void BracketWhitespace() { ? new DMASTLog(callLoc, callParameters[0].Value, null) : new DMASTLog(callLoc, callParameters[1].Value, callParameters[0].Value); } + case "astype": { + if (callParameters.Length != 1 && callParameters.Length != 2) { + Emit(WarningCode.InvalidArgumentCount, callLoc, "astype() requires 1 or 2 arguments"); + return new DMASTInvalidExpression(callLoc); + } + + return callParameters.Length == 1 + ? new DMASTImplicitAsType(callLoc, callParameters[0].Value) + : new DMASTAsType(callLoc, callParameters[0].Value, callParameters[1].Value); + } case "istype": { if (callParameters.Length != 1 && callParameters.Length != 2) { Emit(WarningCode.InvalidArgumentCount, callLoc, "istype() requires 1 or 2 arguments"); diff --git a/DMCompiler/DM/Builders/DMExpressionBuilder.cs b/DMCompiler/DM/Builders/DMExpressionBuilder.cs index 9e25dc50fe..9ef101ba4a 100644 --- a/DMCompiler/DM/Builders/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Builders/DMExpressionBuilder.cs @@ -74,6 +74,7 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe case DMASTNotEqual notEqual: result = BuildNotEqual(notEqual, inferredPath); break; case DMASTDereference deref: result = BuildDereference(deref, inferredPath); break; case DMASTLocate locate: result = BuildLocate(locate, inferredPath); break; + case DMASTImplicitAsType implicitAsType: result = BuildImplicitAsType(implicitAsType, inferredPath); break; case DMASTImplicitIsType implicitIsType: result = BuildImplicitIsType(implicitIsType, inferredPath); break; case DMASTList list: result = BuildList(list, inferredPath); break; case DMASTDimensionalList dimensionalList: result = BuildDimensionalList(dimensionalList, inferredPath); break; @@ -338,25 +339,21 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe BuildExpression(locateCoordinates.Y, inferredPath), BuildExpression(locateCoordinates.Z, inferredPath)); break; + case DMASTAsType asType: { + var lhs = BuildExpression(asType.LHS, inferredPath); + var rhs = BuildExpression(asType.RHS, lhs.Path); + + result = new AsType(asType.Location, lhs, rhs); + break; + } case DMASTIsSaved isSaved: result = new IsSaved(isSaved.Location, BuildExpression(isSaved.Value, inferredPath)); break; case DMASTIsType isType: { - if (isType.RHS is DMASTIdentifier { Identifier: "__IMPLIED_TYPE__" }) { - var expr = BuildExpression(isType.LHS, inferredPath); - if (expr.Path is null) { - result = BadExpression(WarningCode.BadExpression, isType.Location, - "A type could not be inferred!"); - break; - } + var lhs = BuildExpression(isType.LHS, inferredPath); + var rhs = BuildExpression(isType.RHS, lhs.Path); - result = new IsTypeInferred(isType.Location, expr, expr.Path.Value); - break; - } - - result = new IsType(isType.Location, - BuildExpression(isType.LHS, inferredPath), - BuildExpression(isType.RHS, inferredPath)); + result = new IsType(isType.Location, lhs, rhs); break; } @@ -1072,6 +1069,16 @@ private DMExpression BuildLocate(DMASTLocate locate, DreamPath? inferredPath) { return new Locate(locate.Location, pathExpr, container); } + private DMExpression BuildImplicitAsType(DMASTImplicitAsType asType, DreamPath? inferredPath) { + var expr = BuildExpression(asType.Value, inferredPath); + + if (inferredPath is null) { + return BadExpression(WarningCode.BadExpression, asType.Location, "Could not infer a type"); + } + + return new AsTypeInferred(asType.Location, expr, inferredPath.Value); + } + private DMExpression BuildImplicitIsType(DMASTImplicitIsType isType, DreamPath? inferredPath) { var expr = BuildExpression(isType.Value, inferredPath); diff --git a/DMCompiler/DM/DMCodeTree.Vars.cs b/DMCompiler/DM/DMCodeTree.Vars.cs index f7ff73d79c..bc8f31e653 100644 --- a/DMCompiler/DM/DMCodeTree.Vars.cs +++ b/DMCompiler/DM/DMCodeTree.Vars.cs @@ -105,7 +105,7 @@ public override bool TryDefineVar(DMCompiler compiler, int pass) { if (!compiler.DMObjectTree.TryGetDMObject(owner, out var dmObject)) return false; - if (AlreadyExists(compiler, dmObject)) { + if (CheckCantDefine(compiler, dmObject)) { _defined = true; return true; } @@ -162,12 +162,23 @@ private bool HandleInstanceVar(DMCompiler compiler, DMObject dmObject) { return true; } - private bool AlreadyExists(DMCompiler compiler, DMObject dmObject) { - // "type" and "tag" can only be defined in DMStandard - if (VarName is "type" or "tag" && !varDef.Location.InDMStandard) { - compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, - $"Cannot redefine built-in var \"{VarName}\""); - return true; + private bool CheckCantDefine(DMCompiler compiler, DMObject dmObject) { + if (!compiler.Settings.NoStandard) { + var inStandard = varDef.Location.InDMStandard; + + // "type" and "tag" can only be defined in DMStandard + if (VarName is "type" or "tag" && !inStandard) { + compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, + $"Cannot redefine built-in var \"{VarName}\""); + return true; + } + + // Vars on /world and /list can only be defined in DMStandard + if ((dmObject.Path == DreamPath.World || dmObject.Path == DreamPath.List) && !inStandard) { + compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, + $"Cannot define a var on type {dmObject.Path}"); + return true; + } } //DMObjects store two bundles of variables; the statics in GlobalVariables and the non-statics in Variables. diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 4dfa008168..670442f16b 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -969,6 +969,10 @@ public void IsSaved() { WriteOpcode(DreamProcOpcode.IsSaved); } + public void AsType() { + WriteOpcode(DreamProcOpcode.AsType); + } + public void IsType() { WriteOpcode(DreamProcOpcode.IsType); } diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index e11b037cf7..6092ce591d 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -291,6 +291,30 @@ public override void EmitPushValue(ExpressionContext ctx) { } } +// astype(x, y) +internal sealed class AsType(Location location, DMExpression expr, DMExpression path) : DMExpression(location) { + public override void EmitPushValue(ExpressionContext ctx) { + expr.EmitPushValue(ctx); + path.EmitPushValue(ctx); + ctx.Proc.AsType(); + } +} + +// astype(x) +internal sealed class AsTypeInferred(Location location, DMExpression expr, DreamPath path) : DMExpression(location) { + public override void EmitPushValue(ExpressionContext ctx) { + if (!ctx.ObjectTree.TryGetTypeId(path, out var typeId)) { + ctx.Compiler.Emit(WarningCode.ItemDoesntExist, Location, $"Type {path} does not exist"); + + return; + } + + expr.EmitPushValue(ctx); + ctx.Proc.PushType(typeId); + ctx.Proc.AsType(); + } +} + // istype(x, y) internal sealed class IsType(Location location, DMExpression expr, DMExpression path) : DMExpression(location) { public override DMComplexValueType ValType => DMValueType.Num; diff --git a/DMCompiler/DMStandard/Types/List.dm b/DMCompiler/DMStandard/Types/List.dm index 013c1e56c3..e8d25d096d 100644 --- a/DMCompiler/DMStandard/Types/List.dm +++ b/DMCompiler/DMStandard/Types/List.dm @@ -13,6 +13,4 @@ proc/Remove(Item1) proc/RemoveAll(Item1) proc/Swap(Index1, Index2) - - proc/Splice(Start=1,End=0, ...) - set opendream_unimplemented = TRUE + proc/Splice(Start = 1 as num, End = 0 as num, Item1, ...) as null diff --git a/DMCompiler/DMStandard/Types/Sound.dm b/DMCompiler/DMStandard/Types/Sound.dm index a5b678a011..7fa3fb8e05 100644 --- a/DMCompiler/DMStandard/Types/Sound.dm +++ b/DMCompiler/DMStandard/Types/Sound.dm @@ -11,6 +11,9 @@ var/pan = 0 as opendream_unimplemented var/params = null as opendream_unimplemented var/falloff = 1 as opendream_unimplemented + + var/atom/atom as opendream_unimplemented + var/transform as opendream_unimplemented var/x as opendream_unimplemented var/y as opendream_unimplemented var/z as opendream_unimplemented @@ -18,7 +21,7 @@ var/environment as opendream_unimplemented var/echo as opendream_unimplemented var/len as opendream_unimplemented - var/offset as opendream_unimplemented + var/offset var/priority = 0 as opendream_unimplemented var/status = 0 as opendream_unimplemented diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index e36c673aa6..a55e8c46eb 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -84,6 +84,7 @@ proc/roll(ndice = 1, sides) as num proc/round(A, B) as num proc/sha1(input) as text|null proc/shutdown(Addr,Natural = 0) +proc/sign(A) as num proc/sleep(Delay) proc/sorttext(T1, T2) as num proc/sorttextEx(T1, T2) as num @@ -113,7 +114,6 @@ proc/viewers(Depth, Center = usr) as /list proc/walk(Ref, Dir, Lag = 0, Speed = 0) proc/walk_rand(Ref,Lag = 0,Speed = 0) proc/walk_to(Ref, Trg, Min = 0, Lag = 0, Speed = 0) - set opendream_unimplemented = 1 proc/walk_towards(Ref,Trg,Lag=0,Speed=0) proc/winclone(player, window_name, clone_name) proc/winexists(player, control_id) as text diff --git a/DMCompiler/Optimizer/BytecodeOptimizer.cs b/DMCompiler/Optimizer/BytecodeOptimizer.cs index dc1ffb7c7f..cfed5be887 100644 --- a/DMCompiler/Optimizer/BytecodeOptimizer.cs +++ b/DMCompiler/Optimizer/BytecodeOptimizer.cs @@ -12,8 +12,17 @@ internal void Optimize(List input) { RemoveUnreferencedLabels(input); JoinAndForwardLabels(input); RemoveUnreferencedLabels(input); + RemoveImmediateJumps(input); + RemoveUnreferencedLabels(input); _peepholeOptimizer.RunPeephole(input); + + // Run label optimizations again due to possibly removed jumps in peephole optimizers + RemoveUnreferencedLabels(input); + JoinAndForwardLabels(input); + RemoveUnreferencedLabels(input); + RemoveImmediateJumps(input); + RemoveUnreferencedLabels(input); } private void RemoveUnreferencedLabels(List input) { @@ -40,6 +49,22 @@ private void RemoveUnreferencedLabels(List input) { } } + /** + * Removes jumps for which the next element is the jump's destination + */ + private void RemoveImmediateJumps(List input) { + for (int i = input.Count - 2; i >= 0; i--) { + if (input[i] is AnnotatedBytecodeInstruction { Opcode: Bytecode.DreamProcOpcode.Jump } instruction) { + if (input[i + 1] is AnnotatedBytecodeLabel followingLabel) { + AnnotatedBytecodeLabel jumpLabelName = instruction.GetArg(0); + if (jumpLabelName.LabelName == followingLabel.LabelName) { + input.RemoveAt(i); + } + } + } + } + } + private void JoinAndForwardLabels(List input) { Dictionary labelAliases = new(); for (int i = 0; i < input.Count; i++) { diff --git a/DMCompiler/copy_standard.bat b/DMCompiler/copy_standard.bat deleted file mode 100644 index d53a132c2d..0000000000 --- a/DMCompiler/copy_standard.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -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 deleted file mode 100755 index 3532579229..0000000000 --- a/DMCompiler/copy_standard.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -set -xe - -SCRIPT=$(readlink -f "$0") -SCRIPTPATH=$(dirname "$SCRIPT") - -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/net8.0/DMStandard - cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Debug/net8.0/DMStandard -fi - -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/net8.0/DMStandard - cp -r $SCRIPTPATH/DMStandard $SCRIPTPATH/bin/Release/net8.0/DMStandard -fi diff --git a/OpenDreamClient/Audio/DreamSoundEngine.cs b/OpenDreamClient/Audio/DreamSoundEngine.cs index 220863ac31..9178caa914 100644 --- a/OpenDreamClient/Audio/DreamSoundEngine.cs +++ b/OpenDreamClient/Audio/DreamSoundEngine.cs @@ -30,7 +30,7 @@ public void Initialize() { _netManager.Disconnect += DisconnectedFromServer; } - public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume) { + public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume, float offset) { if (_audioSystem == null) _entitySystemManager.Resolve(ref _audioSystem); @@ -59,7 +59,7 @@ public void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sou } 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. + var source = _audioSystem.PlayGlobal(stream, AudioParams.Default.WithVolume(db).WithPlayOffset(offset)); // TODO: Positional audio. if (source == null) { _sawmill.Error($"Failed to play audio ${sound}"); return; @@ -86,7 +86,7 @@ public void 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)); + sound => PlaySound(msg.Channel, msg.Format!.Value, sound, msg.Volume / 100.0f, msg.Offset)); } else { StopChannel(msg.Channel); } diff --git a/OpenDreamClient/Audio/IDreamSoundEngine.cs b/OpenDreamClient/Audio/IDreamSoundEngine.cs index 3ca5361eb9..953b5cc6d1 100644 --- a/OpenDreamClient/Audio/IDreamSoundEngine.cs +++ b/OpenDreamClient/Audio/IDreamSoundEngine.cs @@ -5,7 +5,7 @@ namespace OpenDreamClient.Audio; public interface IDreamSoundEngine { void Initialize(); - void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume); + void PlaySound(int channel, MsgSound.FormatType format, ResourceSound sound, float volume, float offset); void StopChannel(int channel); void StopAllChannels(); } diff --git a/OpenDreamClient/Interface/Controls/UI/ScalingViewport.cs b/OpenDreamClient/Interface/Controls/UI/ScalingViewport.cs index 7ba057e5a2..a5f7395e5a 100644 --- a/OpenDreamClient/Interface/Controls/UI/ScalingViewport.cs +++ b/OpenDreamClient/Interface/Controls/UI/ScalingViewport.cs @@ -111,7 +111,7 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) { _inputManager.ViewportKeyEvent(this, args); } - protected override void Draw(DrawingHandleScreen handle) { + protected override void Draw(IRenderHandle handle) { EnsureViewportCreated(); DebugTools.AssertNotNull(_viewport); @@ -133,7 +133,7 @@ protected override void Draw(DrawingHandleScreen handle) { var drawBox = GetDrawBox(); var drawBoxGlobal = drawBox.Translated(GlobalPixelPosition); _viewport.RenderScreenOverlaysBelow(handle, this, drawBoxGlobal); - handle.DrawTextureRect(_viewport.RenderTarget.Texture, drawBox); + handle.DrawingHandleScreen.DrawTextureRect(_viewport.RenderTarget.Texture, drawBox); _viewport.RenderScreenOverlaysAbove(handle, this, drawBoxGlobal); } diff --git a/OpenDreamClient/Interface/Html/HtmlParser.cs b/OpenDreamClient/Interface/Html/HtmlParser.cs index ef434e83c2..6641c1294f 100644 --- a/OpenDreamClient/Interface/Html/HtmlParser.cs +++ b/OpenDreamClient/Interface/Html/HtmlParser.cs @@ -84,7 +84,8 @@ void PushCurrentText() { } else { tags.Push(tagType); - appendTo.PushTag(new MarkupNode(tagType, null, ParseAttributes(attributes)), selfClosing: attributes[^1] == "/"); + bool isSelfClosing = IsSelfClosing(tagType, attributes); + appendTo.PushTag(new MarkupNode(tagType, null, ParseAttributes(attributes)), selfClosing: isSelfClosing); } break; @@ -139,6 +140,39 @@ void PushCurrentText() { appendTo.Pop(); } + /** + * + * Returns if a tag is written in old self-closing form, or if the tag + * represents a void element, which must have no children + * + */ + private static bool IsSelfClosing(string tagType, string[] attributes) { + if (attributes[^1] == "/") { + return true; + } + + switch (tagType) { + case "area": + case "base": + case "br": + case "col": + case "embed": + case "hr": + case "img": + case "input": + case "link": + case "meta": + case "param": + case "source": + case "track": + case "wbr": + return true; + + default: + return false; + } + } + private static Dictionary ParseAttributes(string[] attributes) { Dictionary parsedAttributes = new(); diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 4237c4eb9a..9183aa44b0 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -212,11 +212,13 @@ 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(); + float offset = outputObject.GetVariable("offset").UnsafeGetValueAsFloat(); DreamValue file = outputObject.GetVariable("file"); var msg = new MsgSound() { Channel = channel, - Volume = volume + Volume = volume, + Offset = offset }; if (!file.TryGetValueAsDreamResource(out var soundResource)) { diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 5c6fbff735..777574d9dc 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1403,39 +1403,57 @@ public static ProcStatus IsInRange(DMProcState state) { return ProcStatus.Continue; } + public static ProcStatus AsType(DMProcState state) { + DreamValue typeValue = state.Pop(); + DreamValue value = state.Pop(); + + state.Push(TypecheckHelper(typeValue, value, true)); + + return ProcStatus.Continue; + } + public static ProcStatus IsType(DMProcState state) { DreamValue typeValue = state.Pop(); DreamValue value = state.Pop(); + + state.Push(TypecheckHelper(typeValue, value, false)); + + return ProcStatus.Continue; + } + + private static DreamValue TypecheckHelper(DreamValue typeValue, DreamValue value, bool doCast) { + // astype() returns null, istype() returns false + DreamValue nullOrFalse = doCast ? DreamValue.Null : DreamValue.False; TreeEntry? type; if (typeValue.TryGetValueAsDreamObject(out var typeObject)) { if (typeObject == null) { - state.Push(DreamValue.False); - return ProcStatus.Continue; + return nullOrFalse; } type = typeObject.ObjectDefinition.TreeEntry; } else if (typeValue.TryGetValueAsAppearance(out _)) { // /image matches an appearance - state.Push(value.TryGetValueAsDreamObject(out _) - ? DreamValue.True - : DreamValue.False); + if (value.TryGetValueAsDreamObject(out var imageObject)) { + return doCast ? new DreamValue(imageObject) : DreamValue.True; + } - return ProcStatus.Continue; + return nullOrFalse; } else if (!typeValue.TryGetValueAsType(out type)) { - state.Push(DreamValue.False); - - return ProcStatus.Continue; + return nullOrFalse; } if (value.TryGetValueAsDreamObject(out var dreamObject) && dreamObject != null) { - state.Push(new DreamValue(dreamObject.IsSubtypeOf(type) ? 1 : 0)); - } else { - state.Push(DreamValue.False); + if (dreamObject.IsSubtypeOf(type)) { + return doCast ? new DreamValue(dreamObject) : DreamValue.True; + } + + return nullOrFalse; } - return ProcStatus.Continue; + return nullOrFalse; } + #endregion Comparisons #region Flow diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index 9b8a536854..f0913cd35b 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -251,6 +251,7 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.Link, DMOpcodeHandlers.Link}, {DreamProcOpcode.Ftp, DMOpcodeHandlers.Ftp}, {DreamProcOpcode.Initial, DMOpcodeHandlers.Initial}, + {DreamProcOpcode.AsType, DMOpcodeHandlers.AsType}, {DreamProcOpcode.IsType, DMOpcodeHandlers.IsType}, {DreamProcOpcode.LocateCoord, DMOpcodeHandlers.LocateCoord}, {DreamProcOpcode.Locate, DMOpcodeHandlers.Locate}, diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index 53c9d38603..542b9323e0 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -85,6 +85,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_round); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sha1); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_shutdown); + objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sign); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sleep); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttext); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttextEx); @@ -114,6 +115,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_walk); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_walk_rand); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_walk_towards); + objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_walk_to); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_winclone); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_winexists); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_winget); @@ -127,6 +129,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Join); objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Remove); objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_RemoveAll); + objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Splice); objectTree.SetNativeProc(objectTree.List, DreamProcNativeList.NativeProc_Swap); objectTree.SetNativeProc(objectTree.Matrix, DreamProcNativeMatrix.NativeProc_Add); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeList.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeList.cs index bb6c98276a..48152e7dd2 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeList.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeList.cs @@ -163,6 +163,36 @@ private static int ListRemove(DreamList list, ReadOnlySpan args) { return itemRemoved; } + [DreamProc("Splice")] + [DreamProcParameter("Start", Type = DreamValueTypeFlag.Float, DefaultValue = 1)] + [DreamProcParameter("End", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + [DreamProcParameter("Item1")] + public static DreamValue NativeProc_Splice(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + int startIndex = bundle.GetArgument(0, "Start").MustGetValueAsInteger(); //1-indexed + int end = bundle.GetArgument(1, "End").MustGetValueAsInteger(); //1-indexed + DreamList list = (DreamList)src!; + + list.Cut(startIndex, end); + + if (startIndex <= 0) startIndex = list.GetLength() + 1; + if (bundle.Arguments.Length < 3) return DreamValue.Null; + + // i = 2 is Item1 + for (var i = 2; i < bundle.Arguments.Length; i++) { + var item = bundle.Arguments[i]; + + if (item.TryGetValueAsDreamList(out var valueList)) { + foreach (DreamValue value in valueList.GetValues()) { + list.Insert(startIndex++, value); + } + } else { + list.Insert(startIndex++, item); + } + } + + return DreamValue.Null; + } + [DreamProc("Swap")] [DreamProcParameter("Index1", Type = DreamValueTypeFlag.Float)] [DreamProcParameter("Index2", Type = DreamValueTypeFlag.Float)] diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index d2af41d246..1dca98cafc 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -2487,6 +2487,22 @@ public static DreamValue NativeProc_shutdown(NativeProc.Bundle bundle, DreamObje return DreamValue.Null; } + [DreamProc("sign")] + [DreamProcParameter("A", Type = DreamValueTypeFlag.Float)] + public static DreamValue NativeProc_sign(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + if (bundle.Arguments.Length != 1) throw new Exception($"expected 1 argument (found {bundle.Arguments.Length})"); + DreamValue arg = bundle.GetArgument(0, "A"); + + // Any non-num returns 0 + if (!arg.TryGetValueAsFloat(out var value)) return new DreamValue(0); + + return value switch { + 0 => new DreamValue(0), + < 0 => new DreamValue(-1), + _ => new DreamValue(1) + }; + } + [DreamProc("sleep")] [DreamProcParameter("Delay", Type = DreamValueTypeFlag.Float)] public static async Task NativeProc_sleep(AsyncNativeProc.State state) { @@ -3275,6 +3291,29 @@ public static DreamValue NativeProc_walk_towards(NativeProc.Bundle bundle, Dream return DreamValue.Null; } + [DreamProc("walk_to")] + [DreamProcParameter("Ref", Type = DreamValueTypeFlag.DreamObject)] + [DreamProcParameter("Trg", Type = DreamValueTypeFlag.DreamObject)] + [DreamProcParameter("Min", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + [DreamProcParameter("Lag", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + [DreamProcParameter("Speed", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + public static DreamValue NativeProc_walk_to(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + if (!bundle.GetArgument(0, "Ref").TryGetValueAsDreamObject(out var refAtom)) + return DreamValue.Null; + + if (!bundle.GetArgument(1, "Trg").TryGetValueAsDreamObject(out var trgAtom)) { + bundle.WalkManager.StopWalks(refAtom); + return DreamValue.Null; + } + + bundle.GetArgument(2, "Min").TryGetValueAsInteger(out var min); + bundle.GetArgument(3, "Lag").TryGetValueAsInteger(out var lag); + bundle.GetArgument(4, "Speed").TryGetValueAsInteger(out var speed); + + bundle.WalkManager.StartWalkTo(refAtom, trgAtom, min, lag, speed); + return DreamValue.Null; + } + [DreamProc("winclone")] [DreamProcParameter("player", Type = DreamValueTypeFlag.DreamObject)] [DreamProcParameter("window_name", Type = DreamValueTypeFlag.String)] diff --git a/OpenDreamRuntime/WalkManager.cs b/OpenDreamRuntime/WalkManager.cs index 560342d20a..ca372e6adf 100644 --- a/OpenDreamRuntime/WalkManager.cs +++ b/OpenDreamRuntime/WalkManager.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading; using OpenDreamRuntime.Map; using OpenDreamRuntime.Objects.Types; @@ -111,4 +112,39 @@ public void StartWalkTowards(DreamObjectMovable movable, DreamObjectAtom target, return DreamValue.Null; }); } + + /// + /// Walk towards the target with pathfinding taken into account + /// + public void StartWalkTo(DreamObjectMovable movable, DreamObjectAtom target, int min, int lag, int speed) { // TODO: Implement speed. Speed=0 uses Ref.step_size + StopWalks(movable); + + lag = Math.Max(lag, 1); // Minimum of 1 tick lag + + CancellationTokenSource cancelSource = new(); + _walkTasks[movable] = cancelSource; + + DreamThread.Run($"walk_to {movable}", async state => { + var moveProc = movable.GetProc("Move"); + + while (true) { + await _scheduler.CreateDelayTicks(lag); + if (cancelSource.IsCancellationRequested) + break; + + var currentLoc = _atomManager.GetAtomPosition(movable); + var targetLoc = _atomManager.GetAtomPosition(target); + var steps = _dreamMapManager.CalculateSteps(currentLoc, targetLoc, min); + using var enumerator = steps.GetEnumerator(); + if (!enumerator.MoveNext()) // No more steps to take + break; + + var dir = enumerator.Current; + var newLoc = DreamProcNativeHelpers.GetStep(_atomManager, _dreamMapManager, movable, dir); + await state.Call(moveProc, movable, null, new(newLoc), new((int)dir)); + } + + return DreamValue.Null; + }); + } } diff --git a/OpenDreamShared/Network/Messages/MsgSound.cs b/OpenDreamShared/Network/Messages/MsgSound.cs index 4a3255759f..b2e4f05c7b 100644 --- a/OpenDreamShared/Network/Messages/MsgSound.cs +++ b/OpenDreamShared/Network/Messages/MsgSound.cs @@ -14,6 +14,7 @@ public enum FormatType : byte { public ushort Channel; public ushort Volume; + public float Offset; public int? ResourceId; public FormatType? Format; // TODO: This should probably be sent along with the sound resource instead somehow //TODO: Frequency and friends @@ -21,6 +22,7 @@ public enum FormatType : byte { public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { Channel = buffer.ReadUInt16(); Volume = buffer.ReadUInt16(); + Offset = buffer.ReadFloat(); if (buffer.ReadBoolean()) { ResourceId = buffer.ReadInt32(); @@ -31,6 +33,7 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { buffer.Write(Channel); buffer.Write(Volume); + buffer.Write(Offset); buffer.Write(ResourceId != null); if (ResourceId != null) { diff --git a/README.md b/README.md index 147d077986..9f2154c342 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![OpenDream](.github/assets/OpenDream.png)](#) +[![OpenDream logo](.github/assets/OpenDream.png)](#) **OpenDream** is a C# project that aims to compile games made in the [DM language], and run them. @@ -13,6 +13,7 @@ A detailed description of differences with BYOND can be found [here](https://git ## Running ### Very Easy Mode + Install the OpenDream vscode extension from [the marketplace](https://marketplace.visualstudio.com/items?itemName=ss13.opendream). Open the folder containing your `.dme` file in vscode. Press the "Start debugging (F5)" button. The extension will automatically handle getting the latest OpenDream and SS14 Launcher binaries, compile and run everything for you. @@ -33,28 +34,31 @@ There's 3 main parts: Compiler, Server, and Client: ## Building -The first step to building OpenDream is initializing the submodule for the game engine, [Robust Toolbox](https://github.com/space-wizards/RobustToolbox). +The first step to building OpenDream is initializing the submodule for the game engine, [Robust Toolbox](https://github.com/space-wizards/RobustToolbox). To do this, simply run `git submodule update --init --recursive` in git bash and let it finish. -**OpenDream requires .NET 8.** You can check your version by running `dotnet --version`. It should be at least `8.0.0`. +**OpenDream requires .NET 9.** You can check your version by running `dotnet --version`. It should be at least `9.0.0`. To build, one can use a C# compiler (such as MSBuild) to compile the various projects described in the solution. To use the .NET build system, simply run `dotnet build -c Release` in the OpenDream directory. This will build all of the solutions in release mode and put the resultant binaries in `bin` ## Testing + OpenDream makes use of a unit testing framework. You can run these unit tests by running `dotnet test` in the OpenDream directory. This will run all of the RobustToolbox tests, as well as the DM language tests which can be found under `Content.Tests/DMProject/Tests/`. To add to the unit tests, simply create a `.dm` file under the Tests directory with `/proc/RunTest()` as the entry point. Optionally you can also add flags to the test, such as `// COMPILE ERROR` to mark that this test should cause a compile error. ## Requests for Comment + New features unrelated to BYOND parity go through an RFC process to solicit input from the wider community. The RFCs and more info on the RFC process can be found [here](https://github.com/OpenDreamProject/rfcs). ## Screenshots -![](./.github/assets/screenshot.png?raw=true) + +![/tg/station screenshot](./.github/assets/screenshot.png?raw=true) _[/tg/station](https://github.com/tgstation/tgstation)_ -![](./.github/assets/screenshot2.png?raw=true) +![Paradise screenshot](./.github/assets/screenshot2.png?raw=true) _Version of [Paradise with a 64-bit rustg DLL](https://github.com/ike709/Paradise/tree/rustg_64)_ -![](./.github/assets/screenshot3.png?raw=true) +![Goonstation screenshot](./.github/assets/screenshot3.png?raw=true) _[Goonstation](https://github.com/goonstation/goonstation)_ [DM Language]: http://secure.byond.com/ diff --git a/RobustToolbox b/RobustToolbox index 0c0d878777..7104a4f459 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 0c0d8787776ae1a0c5c3c00cf04fceaa53d910f3 +Subproject commit 7104a4f4594149f5c09aeefc34275b1cbac4e297