From 0e13edb70258e6f62746d245f0ea7b453589d699 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 26 Nov 2023 18:26:34 -0500 Subject: [PATCH 01/11] Add YieldOrder tests --- .../DMProject/Tests/Sleeping/YieldOrder.dm | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm diff --git a/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm b/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm new file mode 100644 index 0000000000..4ff554ee8f --- /dev/null +++ b/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm @@ -0,0 +1,61 @@ +var/counter +#define ExpectOrder(n) ASSERT(++counter == ##n) + +/proc/BackgroundSleep(delay, expect) + set waitfor = FALSE + sleep(delay) + world.log << "Expect: [expect]" + ExpectOrder(expect) + +#define MODE_INLINE 0 // spawn +#define MODE_BACKGROUND 1 // set waitfor = FALSE + sleep +#define MODE_RAND 2 // random seeded + +#define TestSleep(delay, expect) if(mode == MODE_INLINE || (mode == MODE_RAND && prob(50))){ spawn(##delay) { ExpectOrder(##expect); } } else { BackgroundSleep(##delay, ##expect); } + +/proc/TestSequence(mode) + counter = 0 + var/start_tick = world.time + + TestSleep(0, 2) + ExpectOrder(1) + sleep(0) + ExpectOrder(3) + + TestSleep(-1, 4) + ExpectOrder(5) + + TestSleep(0, 6) + sleep(-1) + ExpectOrder(7) + + TestSleep(-1, 8) + ExpectOrder(9) + sleep(-1) + ExpectOrder(10) + + TestSleep(1, 13) + sleep(-1) + ExpectOrder(11) + sleep(0) + ExpectOrder(12) + + ASSERT(world.time == start_tick) + + sleep(1) + ExpectOrder(14) + +/proc/RunTest() + world.log << "Inline:" + TestSequence(MODE_INLINE) + + // test fails after this point + return + + world.log << "Background:" + TestSequence(MODE_BACKGROUND) + + rand_seed(22475) + for(var/i in 1 to 10000) + world.log << "Rand-[i]:" + TestSequence(MODE_RAND) From 4f0b5344edf01a243d4852bc01884a2bc70575a9 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 26 Nov 2023 18:25:35 -0500 Subject: [PATCH 02/11] Fix yield scheduling --- .../DMProject/Tests/Sleeping/YieldOrder.dm | 3 --- OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 14 +++----------- OpenDreamRuntime/Procs/ProcScheduler.Delays.cs | 14 +++++++++----- OpenDreamRuntime/Procs/ProcScheduler.cs | 4 +++- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm b/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm index 4ff554ee8f..99513df4a1 100644 --- a/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm +++ b/Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm @@ -49,9 +49,6 @@ world.log << "Inline:" TestSequence(MODE_INLINE) - // test fails after this point - return - world.log << "Background:" TestSequence(MODE_BACKGROUND) diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index d3d0ff4675..0d600af68a 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1703,20 +1703,12 @@ public static ProcStatus Spawn(DMProcState state) { // and have state.Spawn return a ProcState instead DreamThread newContext = state.Spawn(); - //Negative delays mean the spawned code runs immediately - if (delayMilliseconds < 0) { + async void Wait() { + await state.ProcScheduler.CreateDelay(delay); newContext.Resume(); - // TODO: Does the rest of the proc get scheduled? - // Does the value of the delay mean anything? - } else { - async void Wait() { - await state.ProcScheduler.CreateDelay(delay); - newContext.Resume(); - } - - Wait(); } + Wait(); state.Jump(jumpTo); return ProcStatus.Continue; } diff --git a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs index fd2033230d..71339b2010 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs @@ -40,12 +40,16 @@ public Task CreateDelay(float deciseconds) { /// The amount of ticks to sleep. /// public Task CreateDelayTicks(int ticks) { + // When the delay is <= zero, we should run again in the current tick. + // Now, BYOND apparently does have a difference between 0 and -1. See https://github.com/OpenDreamProject/OpenDream/issues/1262#issuecomment-1563663041 + // They both delay execution and allow other sleeping procs in the current tick to run immediately. + // We achieve this by putting the proc on the _deferredTasks lists, so it can be immediately executed again. + if (ticks < 0 && !HasProcsQueued) { + // special case, only yields when there is more work to do + return Task.CompletedTask; + } + if (ticks <= 0) { - // When the delay is <= zero, we should run again in the current tick. - // Now, BYOND apparently does have a difference between 0 and -1, but we're not quite sure what it is yet. - // This is "good enough" for now. - // They both delay execution and allow other sleeping procs in the current tick to run immediately. - // We achieve this by putting the proc on the _deferredTasks lists, so it can be immediately executed again. var defTcs = new TaskCompletionSource(); _deferredTasks.Enqueue(defTcs); diff --git a/OpenDreamRuntime/Procs/ProcScheduler.cs b/OpenDreamRuntime/Procs/ProcScheduler.cs index f4ae1566d5..998a291dad 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.cs @@ -26,6 +26,8 @@ public sealed partial class ProcScheduler { private readonly Queue _scheduled = new(); private AsyncNativeProc.State? _current; + bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0; + public Task Schedule(AsyncNativeProc.State state, Func> taskFunc) { async Task Foo() { state.Result = await taskFunc(state); @@ -51,7 +53,7 @@ public void Process() { // If a proc calls sleep(1) or such, it gets put into _deferredTasks. // When we drain the _deferredTasks lists, it'll indirectly schedule things into _scheduled again. // This should all happen synchronously (see above). - while (_scheduled.Count > 0 || _deferredTasks.Count > 0) { + while (HasProcsQueued) { while (_scheduled.TryDequeue(out _current)) { _current.SafeResume(); } From 0911a5e454cf11958d1654adbde6c57c41f7a132 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 26 Nov 2023 21:53:56 -0500 Subject: [PATCH 03/11] Fix ProcScheduler state poisoning between tests --- Content.Tests/DMTests.cs | 4 +++- OpenDreamRuntime/Procs/ProcScheduler.Delays.cs | 2 ++ OpenDreamRuntime/Procs/ProcScheduler.cs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 59b69dde28..8fb03bf6bc 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using OpenDreamRuntime; using OpenDreamRuntime.Objects; +using OpenDreamRuntime.Procs; using OpenDreamRuntime.Rendering; using OpenDreamShared.Rendering; using Robust.Shared.Asynchronous; @@ -28,6 +29,7 @@ public sealed class DMTests : ContentUnitTest { [Dependency] private readonly DreamManager _dreamMan = default!; [Dependency] private readonly DreamObjectTree _objectTree = default!; + [Dependency] private readonly ProcScheduler _procScheduler = default!; [Dependency] private readonly ITaskManager _taskManager = default!; [Flags] @@ -138,7 +140,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { watch.Start(); // Tick until our inner call has finished - while (!callTask.IsCompleted) { + while (!callTask.IsCompleted || _procScheduler.HasProcsQueued || _procScheduler.HasProcsSleeping) { _dreamMan.Update(); _taskManager.ProcessPendingTasks(); diff --git a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs index 71339b2010..fa2727d165 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs @@ -14,6 +14,8 @@ public sealed partial class ProcScheduler { // This is for deferred tasks that need to fire in the current tick. private readonly Queue _deferredTasks = new(); + public bool HasProcsSleeping => _tickers.Count > 0; + /// /// Create a task that will delay by an amount of time, following the rules for sleep and spawn. /// diff --git a/OpenDreamRuntime/Procs/ProcScheduler.cs b/OpenDreamRuntime/Procs/ProcScheduler.cs index 998a291dad..2daee6f2a9 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.cs @@ -26,7 +26,7 @@ public sealed partial class ProcScheduler { private readonly Queue _scheduled = new(); private AsyncNativeProc.State? _current; - bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0; + public bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0; public Task Schedule(AsyncNativeProc.State state, Func> taskFunc) { async Task Foo() { From f0e4f27460532b5f8da28cf3ace7d3c1de720503 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 26 Nov 2023 22:00:53 -0500 Subject: [PATCH 04/11] Up DMTests timeout x10 for slow Windows runners --- Content.Tests/DMTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 8fb03bf6bc..72c62cf541 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -144,7 +144,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { _dreamMan.Update(); _taskManager.ProcessPendingTasks(); - if (watch.Elapsed.TotalMilliseconds > 500) { + if (watch.Elapsed.TotalSeconds > 5) { Assert.Fail("Test timed out"); } } From 86fb39461732d3d688d3ac13bcb1e60ae2af7817 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 1 Jan 2024 12:29:04 -0500 Subject: [PATCH 05/11] Redo sleeping as an opcode - Remove `sleep` from DMStandard. - Two new opcodes Sleep and BackgroundSleep (which is just `sleep -1`, much faster than popping a `float` off for background sleeps). - Create new lightweight async proc state for sleeping. - Compiler/runtime adjustments to handle new opcodes. --- DMCompiler/Bytecode/DreamProcOpcode.cs | 2 + DMCompiler/Compiler/DM/DMAST.cs | 17 ++++ DMCompiler/Compiler/DM/DMLexer.cs | 1 + DMCompiler/Compiler/DM/DMParser.cs | 19 +++++ DMCompiler/DM/DMProc.cs | 21 +++-- DMCompiler/DM/Visitors/DMASTSimplifier.cs | 4 + DMCompiler/DM/Visitors/DMProcBuilder.cs | 16 ++++ DMCompiler/DMStandard/_Standard.dm | 1 - OpenDreamRuntime/Procs/AsyncNativeProc.cs | 4 +- OpenDreamRuntime/Procs/AsyncProcState.cs | 5 ++ OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 81 ++++++++++++++++++- OpenDreamRuntime/Procs/DMProc.cs | 2 + .../Procs/Native/DreamProcNative.cs | 1 - .../Procs/Native/DreamProcNativeRoot.cs | 10 --- OpenDreamRuntime/Procs/ProcDecoder.cs | 1 + OpenDreamRuntime/Procs/ProcScheduler.cs | 20 +++-- OpenDreamShared/Compiler/Token.cs | 1 + 17 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 OpenDreamRuntime/Procs/AsyncProcState.cs diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index 5c7a1599a8..7e59861f84 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -139,6 +139,8 @@ public enum DreamProcOpcode : byte { Log = 0x81, LogE = 0x82, Abs = 0x83, + [OpcodeMetadata(stackDelta: -1)] Sleep = 0x84, + BackgroundSleep = 0x85, } /// diff --git a/DMCompiler/Compiler/DM/DMAST.cs b/DMCompiler/Compiler/DM/DMAST.cs index 9ddaadd626..a325a21bee 100644 --- a/DMCompiler/Compiler/DM/DMAST.cs +++ b/DMCompiler/Compiler/DM/DMAST.cs @@ -78,6 +78,10 @@ public void VisitProcStatementSpawn(DMASTProcStatementSpawn statementSpawn) { throw new NotImplementedException(); } + public void VisitProcStatementSleep(DMASTProcStatementSleep statementSleep) { + throw new NotImplementedException(); + } + public void VisitProcStatementIf(DMASTProcStatementIf statementIf) { throw new NotImplementedException(); } @@ -920,6 +924,19 @@ public override void Visit(DMASTVisitor visitor) { } } + public sealed class DMASTProcStatementSleep : DMASTProcStatement { + public DMASTExpression Delay; + + public DMASTProcStatementSleep(Location location, DMASTExpression delay) : + base(location) { + Delay = delay; + } + + public override void Visit(DMASTVisitor visitor) { + visitor.VisitProcStatementSleep(this); + } + } + public sealed class DMASTProcStatementSpawn : DMASTProcStatement { public DMASTExpression Delay; public readonly DMASTProcBlockInner Body; diff --git a/DMCompiler/Compiler/DM/DMLexer.cs b/DMCompiler/Compiler/DM/DMLexer.cs index fbd0741f6a..4ecf96b496 100644 --- a/DMCompiler/Compiler/DM/DMLexer.cs +++ b/DMCompiler/Compiler/DM/DMLexer.cs @@ -60,6 +60,7 @@ public sealed class DMLexer : TokenLexer { { "call", TokenType.DM_Call }, { "call_ext", TokenType.DM_Call}, { "spawn", TokenType.DM_Spawn }, + { "sleep", TokenType.DM_Sleep }, { "goto", TokenType.DM_Goto }, { "step", TokenType.DM_Step }, { "try", TokenType.DM_Try }, diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 7d667135f4..b96a97a0b4 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -100,6 +100,7 @@ public DMParser(DMLexer lexer) : base(lexer) { TokenType.DM_Null, TokenType.DM_Switch, TokenType.DM_Spawn, + TokenType.DM_Sleep, TokenType.DM_Do, TokenType.DM_While, TokenType.DM_For, @@ -715,6 +716,7 @@ public DMASTFile File() { procStatement ??= Switch(); procStatement ??= Continue(); procStatement ??= Break(); + procStatement ??= Sleep(); procStatement ??= Spawn(); procStatement ??= While(); procStatement ??= DoWhile(); @@ -1059,6 +1061,23 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { } } + public DMASTProcStatementSleep? Sleep() { + var loc = Current().Location; + + if (Check(TokenType.DM_Sleep)) { + Whitespace(); + bool hasParenthesis = Check(TokenType.DM_LeftParenthesis); + Whitespace(); + DMASTExpression? delay = Expression(); + if (delay == null) Error("Expected delay to sleep for"); + if (hasParenthesis) ConsumeRightParenthesis(); + + return new DMASTProcStatementSleep(loc, delay ?? new DMASTConstantInteger(loc, 0)); + } else { + return null; + } + } + public DMASTProcStatementIf? If() { var loc = Current().Location; diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 4f7320cb1e..3e44a85266 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -459,20 +459,19 @@ public void MarkLoopContinue(string loopLabel) { AddLabel($"{loopLabel}_continue"); } - public void BackgroundSleep() { - // TODO This seems like a bad way to handle background, doesn't it? - - if ((Attributes & ProcAttributes.Background) == ProcAttributes.Background) { - if (!DMObjectTree.TryGetGlobalProc("sleep", out var sleepProc)) { - throw new CompileErrorException(Location, "Cannot do a background sleep without a sleep proc"); - } - - PushFloat(-1); // argument given to sleep() - Call(DMReference.CreateGlobalProc(sleepProc.Id), DMCallArgumentsType.FromStack, 1); - Pop(); // Pop the result of the sleep call + public void SleepDelayPushed() => WriteOpcode(DreamProcOpcode.Sleep); + + public void Sleep(float delay) { + if(delay == -1.0f) // yielding + WriteOpcode(DreamProcOpcode.BackgroundSleep); + else { + PushFloat(delay); + WriteOpcode(DreamProcOpcode.Sleep); } } + public void BackgroundSleep() => WriteOpcode(DreamProcOpcode.BackgroundSleep); + public void LoopJumpToStart(string loopLabel) { BackgroundSleep(); Jump($"{loopLabel}_start"); diff --git a/DMCompiler/DM/Visitors/DMASTSimplifier.cs b/DMCompiler/DM/Visitors/DMASTSimplifier.cs index 77c8b7b371..d3aaea9794 100644 --- a/DMCompiler/DM/Visitors/DMASTSimplifier.cs +++ b/DMCompiler/DM/Visitors/DMASTSimplifier.cs @@ -124,6 +124,10 @@ public void VisitProcStatementSpawn(DMASTProcStatementSpawn statementSpawn) { statementSpawn.Body.Visit(this); } + public void VisitProcStatementSleep(DMASTProcStatementSleep statementSleep) { + SimplifyExpression(ref statementSleep.Delay); + } + public void VisitProcStatementGoto(DMASTProcStatementGoto statementGoto) { } diff --git a/DMCompiler/DM/Visitors/DMProcBuilder.cs b/DMCompiler/DM/Visitors/DMProcBuilder.cs index 14e540c08f..31ddc228c6 100644 --- a/DMCompiler/DM/Visitors/DMProcBuilder.cs +++ b/DMCompiler/DM/Visitors/DMProcBuilder.cs @@ -121,6 +121,7 @@ public void ProcessStatement(DMASTProcStatement statement) { case DMASTProcStatementBreak statementBreak: ProcessStatementBreak(statementBreak); break; case DMASTProcStatementDel statementDel: ProcessStatementDel(statementDel); break; case DMASTProcStatementSpawn statementSpawn: ProcessStatementSpawn(statementSpawn); break; + case DMASTProcStatementSleep statementSleep: ProcessStatementSleep(statementSleep); break; case DMASTProcStatementReturn statementReturn: ProcessStatementReturn(statementReturn); break; case DMASTProcStatementIf statementIf: ProcessStatementIf(statementIf); break; case DMASTProcStatementFor statementFor: ProcessStatementFor(statementFor); break; @@ -317,6 +318,21 @@ public void ProcessStatementSpawn(DMASTProcStatementSpawn statementSpawn) { _proc.AddLabel(afterSpawnLabel); } + public void ProcessStatementSleep(DMASTProcStatementSleep statementSleep) { + var expr = DMExpression.Create(_dmObject, _proc, statementSleep.Delay); + if (expr.TryAsConstant(out var constant)) { + if (constant is Number constantNumber) { + _proc.Sleep(constantNumber.Value); + return; + } + + constant.EmitPushValue(_dmObject, _proc); + } else + expr.EmitPushValue(_dmObject, _proc); + + _proc.SleepDelayPushed(); + } + public void ProcessStatementVarDeclaration(DMASTProcStatementVarDeclaration varDeclaration) { if (varDeclaration.IsGlobal) { return; } //Currently handled by DMObjectBuilder diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index 8c730da05b..6447063a5d 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -80,7 +80,6 @@ proc/roll(ndice = 1, sides) proc/round(A, B) proc/sha1(input) proc/shutdown(Addr,Natural = 0) -proc/sleep(Delay) proc/sorttext(T1, T2) proc/sorttextEx(T1, T2) proc/sound(file, repeat = 0, wait, channel, volume) diff --git a/OpenDreamRuntime/Procs/AsyncNativeProc.cs b/OpenDreamRuntime/Procs/AsyncNativeProc.cs index cc9576358f..22c703cb31 100644 --- a/OpenDreamRuntime/Procs/AsyncNativeProc.cs +++ b/OpenDreamRuntime/Procs/AsyncNativeProc.cs @@ -9,7 +9,7 @@ namespace OpenDreamRuntime.Procs { public sealed class AsyncNativeProc : DreamProc { - public sealed class State : ProcState { + public sealed class State : AsyncProcState { public static readonly Stack Pool = new(); // IoC dependencies instead of proc fields because _proc can be null @@ -52,7 +52,7 @@ public void Initialize(AsyncNativeProc? proc, Func> task } // Used to avoid reentrant resumptions in our proc - public void SafeResume() { + public override void SafeResume() { if (_inResume) { return; } diff --git a/OpenDreamRuntime/Procs/AsyncProcState.cs b/OpenDreamRuntime/Procs/AsyncProcState.cs new file mode 100644 index 0000000000..b08d014d3c --- /dev/null +++ b/OpenDreamRuntime/Procs/AsyncProcState.cs @@ -0,0 +1,5 @@ +namespace OpenDreamRuntime.Procs { + public abstract class AsyncProcState : ProcState { + public abstract void SafeResume(); + } +} diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 0d600af68a..816932a0bf 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -12,6 +12,8 @@ using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; using Robust.Shared.Random; + +using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; using Vector4 = Robust.Shared.Maths.Vector4; namespace OpenDreamRuntime.Procs { @@ -167,7 +169,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 { @@ -1713,6 +1715,83 @@ async void Wait() { return ProcStatus.Continue; } + public static ProcStatus Sleep(DMProcState state) { + state.Pop().TryGetValueAsFloat(out var delay); + return SleepCore( + state, + state.ProcScheduler.CreateDelay(delay)); + } + + public static ProcStatus BackgroundSleep(DMProcState state) => SleepCore( + state, + state.ProcScheduler.CreateDelayTicks(-1)); + + static ProcStatus SleepCore(DMProcState state, Task delay) { + if (delay.IsCompleted) + return ProcStatus.Continue; // fast path, skip state creation + + if (!SleepState.Pool.TryPop(out var sleepState)) { + sleepState = new SleepState(); + } + + return sleepState.Initialize(state.Thread, state.Proc, delay); + } + + // "proc state" we just need something to hold the delay task + sealed class SleepState : AsyncProcState { + public static readonly Stack Pool = new(); + + [Dependency] public readonly ProcScheduler ProcScheduler = null!; + + DreamProc? _proc; + Task? _task; + bool inResume; + + public SleepState() { + IoCManager.InjectDependencies(this); + } + + public ProcStatus Initialize(DreamThread thread, DMProc proc, Task delay) { + Thread = thread; + _proc = proc; + _task = ProcScheduler.Schedule(this, delay); + thread.PushProcState(this); + return thread.HandleDefer(); + } + + public override void Dispose() { + base.Dispose(); + Thread = null!; + _proc = null; + _task = null; + Pool.Push(this); + } + + public override DreamProc? Proc => _proc; + + public override void AppendStackFrame(StringBuilder builder) { + builder.Append("/proc/sleep"); + } + + // a sleep is always the top of a thread so it's always safe to resume + public override void SafeResume() => Thread.Resume(); + + public override ProcStatus Resume() { + if (_task!.IsCompleted) { + // read before we get disposed when popped off + var exception = _task.Exception; + Thread.PopProcState(); + if (exception != null) { + throw exception; + } + + return ProcStatus.Returned; + } + + return Thread.HandleDefer(); + } + } + public static ProcStatus DebuggerBreakpoint(DMProcState state) { return state.DebugManager.HandleBreakpoint(state); } diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index b8c155ed82..452acc2cfd 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -249,6 +249,8 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.Locate, DMOpcodeHandlers.Locate}, {DreamProcOpcode.IsNull, DMOpcodeHandlers.IsNull}, {DreamProcOpcode.Spawn, DMOpcodeHandlers.Spawn}, + {DreamProcOpcode.Sleep, DMOpcodeHandlers.Sleep}, + {DreamProcOpcode.BackgroundSleep, DMOpcodeHandlers.BackgroundSleep}, {DreamProcOpcode.OutputReference, DMOpcodeHandlers.OutputReference}, {DreamProcOpcode.Output, DMOpcodeHandlers.Output}, {DreamProcOpcode.JumpIfNullDereference, DMOpcodeHandlers.JumpIfNullDereference}, diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index c078826a2e..c7f347c6c5 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -79,7 +79,6 @@ 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_sleep); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttext); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttextEx); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sound); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 4489bcd9a6..c21e9e6666 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -2145,16 +2145,6 @@ public static DreamValue NativeProc_shutdown(NativeProc.Bundle bundle, DreamObje return DreamValue.Null; } - [DreamProc("sleep")] - [DreamProcParameter("Delay", Type = DreamValueTypeFlag.Float)] - public static async Task NativeProc_sleep(AsyncNativeProc.State state) { - state.GetArgument(0, "Delay").TryGetValueAsFloat(out float delay); - - await state.ProcScheduler.CreateDelay(delay); - - return DreamValue.Null; - } - [DreamProc("sorttext")] [DreamProcParameter("T1", Type = DreamValueTypeFlag.String)] [DreamProcParameter("T2", Type = DreamValueTypeFlag.String)] diff --git a/OpenDreamRuntime/Procs/ProcDecoder.cs b/OpenDreamRuntime/Procs/ProcDecoder.cs index 3386663015..6a01d4e06e 100644 --- a/OpenDreamRuntime/Procs/ProcDecoder.cs +++ b/OpenDreamRuntime/Procs/ProcDecoder.cs @@ -124,6 +124,7 @@ public ITuple DecodeInstruction() { case DreamProcOpcode.PickWeighted: case DreamProcOpcode.PickUnweighted: case DreamProcOpcode.Spawn: + case DreamProcOpcode.Sleep: case DreamProcOpcode.BooleanOr: case DreamProcOpcode.BooleanAnd: case DreamProcOpcode.SwitchCase: diff --git a/OpenDreamRuntime/Procs/ProcScheduler.cs b/OpenDreamRuntime/Procs/ProcScheduler.cs index 2daee6f2a9..5c1f39d439 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.cs @@ -22,22 +22,32 @@ namespace OpenDreamRuntime.Procs; public sealed partial class ProcScheduler { - private readonly HashSet _sleeping = new(); - private readonly Queue _scheduled = new(); - private AsyncNativeProc.State? _current; + private readonly HashSet _sleeping = new(); + private readonly Queue _scheduled = new(); + private AsyncProcState? _current; public bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0; public Task Schedule(AsyncNativeProc.State state, Func> taskFunc) { async Task Foo() { state.Result = await taskFunc(state); - if (!_sleeping.Remove(state)) + } + + return Schedule( + state, + Foo()); + } + + public Task Schedule(AsyncProcState state, Task asyncTask) { + async Task Bar() { + await asyncTask; + if(!_sleeping.Remove(state)) return; _scheduled.Enqueue(state); } - var task = Foo(); + var task = Bar(); if (!task.IsCompleted) // No need to schedule the proc if it's already finished _sleeping.Add(state); diff --git a/OpenDreamShared/Compiler/Token.cs b/OpenDreamShared/Compiler/Token.cs index 979dfd9840..d55019ebf7 100644 --- a/OpenDreamShared/Compiler/Token.cs +++ b/OpenDreamShared/Compiler/Token.cs @@ -121,6 +121,7 @@ public enum TokenType : byte { DM_Slash, DM_SlashEquals, DM_Spawn, + DM_Sleep, DM_Star, DM_StarEquals, DM_StarStar, From 8d7ebebad5c23ecaad2fad6c8d5864157fec10fe Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 1 Jan 2024 12:34:43 -0500 Subject: [PATCH 06/11] Emit constant folded `BackgroundSleep` opcode on negative values other than `-1.0f` --- DMCompiler/DM/DMProc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 3e44a85266..c64bea6904 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -462,7 +462,7 @@ public void MarkLoopContinue(string loopLabel) { public void SleepDelayPushed() => WriteOpcode(DreamProcOpcode.Sleep); public void Sleep(float delay) { - if(delay == -1.0f) // yielding + if(delay <= 0) // yielding WriteOpcode(DreamProcOpcode.BackgroundSleep); else { PushFloat(delay); From 1e4dd8173e0e30ee88e35bb9f333445550f37ea6 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 9 Feb 2024 09:02:11 -0500 Subject: [PATCH 07/11] Clear ProcScheduler state each test run --- Content.Tests/DMTests.cs | 8 ++++++-- OpenDreamRuntime/Procs/ProcScheduler.cs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 961c5305bd..079b1188b1 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using OpenDreamRuntime; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Procs; -using OpenDreamRuntime.Rendering; -using OpenDreamShared.Rendering; using Robust.Shared.Asynchronous; using Robust.Shared.IoC; using Robust.Shared.Timing; @@ -74,6 +73,8 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { return; } + _procScheduler.ClearState(); + 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(); @@ -99,6 +100,9 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { Assert.That(returned?.IsTruthy(), Is.True, "Test was expected to return TRUE"); } + var threads = _procScheduler.InspectThreads().ToList(); + Assert.That(threads.Count == 0 && !_procScheduler.HasProcsSleeping && !_procScheduler.HasProcsQueued, $"One or more threads did not finish!"); + Cleanup(compiledFile); TestContext.WriteLine($"--- PASS {sourceFile}"); } finally { diff --git a/OpenDreamRuntime/Procs/ProcScheduler.cs b/OpenDreamRuntime/Procs/ProcScheduler.cs index 5c1f39d439..f3f773a37d 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.cs @@ -74,6 +74,17 @@ public void Process() { } } + /// + /// Clears all ProcScheduler state + /// + public void ClearState() { + _current = null; + _deferredTasks.Clear(); + _sleeping.Clear(); + _scheduled.Clear(); + _tickers.Clear(); + } + public IEnumerable InspectThreads() { // TODO: We shouldn't need to check if Thread is null here // I think we're keeping disposed states somewhere here From 38e1ff416870cb916baac2b04cb4ad8c291aff79 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 10 Feb 2024 15:44:09 -0500 Subject: [PATCH 08/11] Create `IOpenDreamGameTiming` --- Content.Tests/ContentUnitTest.cs | 5 ++-- Content.Tests/DMTests.cs | 11 +++++++- Content.Tests/DummyOpenDreamGameTiming.cs | 26 +++++++++++++++++++ .../Rendering/ClientImagesSystem.cs | 1 - OpenDreamRuntime/DreamManager.cs | 2 +- .../Objects/Types/DreamObjectWorld.cs | 5 ++-- .../Procs/IOpenDreamGameTiming.cs | 15 +++++++++++ OpenDreamRuntime/Procs/OpenDreamGameTiming.cs | 20 ++++++++++++++ .../Procs/ProcScheduler.Delays.cs | 2 +- OpenDreamRuntime/ServerContentIoC.cs | 1 + 10 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 Content.Tests/DummyOpenDreamGameTiming.cs create mode 100644 OpenDreamRuntime/Procs/IOpenDreamGameTiming.cs create mode 100644 OpenDreamRuntime/Procs/OpenDreamGameTiming.cs diff --git a/Content.Tests/ContentUnitTest.cs b/Content.Tests/ContentUnitTest.cs index c102584fb6..647d58e07e 100644 --- a/Content.Tests/ContentUnitTest.cs +++ b/Content.Tests/ContentUnitTest.cs @@ -3,8 +3,8 @@ using System.Reflection; using OpenDreamClient; using OpenDreamRuntime; +using OpenDreamRuntime.Procs; using OpenDreamRuntime.Rendering; -using OpenDreamShared; using OpenDreamShared.Rendering; using Robust.Shared.Analyzers; using Robust.Shared.IoC; @@ -23,11 +23,10 @@ public class ContentUnitTest : RobustUnitTest { protected override void OverrideIoC() { base.OverrideIoC(); - SharedOpenDreamIoC.Register(); - if (Project == UnitTestProject.Server) { ServerContentIoC.Register(unitTests: true); IoCManager.Register(); + IoCManager.Register(); } else if (Project == UnitTestProject.Client) { ClientContentIoC.Register(); } diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 079b1188b1..200235c47e 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -19,11 +19,14 @@ public sealed class DMTests : ContentUnitTest { private const string InitializeEnvironment = "./environment.dme"; private const string TestsDirectory = "Tests"; + [Dependency] IOpenDreamGameTiming _gameTiming = default!; [Dependency] private readonly DreamManager _dreamMan = default!; [Dependency] private readonly DreamObjectTree _objectTree = default!; [Dependency] private readonly ProcScheduler _procScheduler = default!; [Dependency] private readonly ITaskManager _taskManager = default!; + DummyOpenDreamGameTiming GameTiming => (DummyOpenDreamGameTiming)_gameTiming; + [Flags] public enum DMTestFlags { NoError = 0, // Should run without errors @@ -74,6 +77,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { } _procScheduler.ClearState(); + GameTiming.CurTick = GameTick.Zero; Assert.That(compiledFile is not null && File.Exists(compiledFile), "Failed to compile DM source file"); Assert.That(_dreamMan.LoadJson(compiledFile), $"Failed to load {compiledFile}"); @@ -81,10 +85,13 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { (bool successfulRun, DreamValue? returned, Exception? exception) = RunTest(); + int expectedThreads; if (testFlags.HasFlag(DMTestFlags.NoReturn)) { Assert.That(returned.HasValue, Is.False, "proc returned unexpectedly"); + expectedThreads = 1; } else { Assert.That(returned.HasValue, "proc did not return (did it hit an exception?)"); + expectedThreads = 0; } if (testFlags.HasFlag(DMTestFlags.RuntimeError)) { @@ -101,7 +108,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { } var threads = _procScheduler.InspectThreads().ToList(); - Assert.That(threads.Count == 0 && !_procScheduler.HasProcsSleeping && !_procScheduler.HasProcsQueued, $"One or more threads did not finish!"); + Assert.That(threads.Count == expectedThreads && !_procScheduler.HasProcsSleeping && !_procScheduler.HasProcsQueued, $"One or more threads did not finish!"); Cleanup(compiledFile); TestContext.WriteLine($"--- PASS {sourceFile}"); @@ -136,6 +143,8 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { _dreamMan.Update(); _taskManager.ProcessPendingTasks(); + GameTiming.CurTick = new GameTick(_gameTiming.CurTick.Value + 1); + if (watch.Elapsed.TotalSeconds > 5) { Assert.Fail("Test timed out"); } diff --git a/Content.Tests/DummyOpenDreamGameTiming.cs b/Content.Tests/DummyOpenDreamGameTiming.cs new file mode 100644 index 0000000000..016e1fc1ec --- /dev/null +++ b/Content.Tests/DummyOpenDreamGameTiming.cs @@ -0,0 +1,26 @@ +using OpenDreamRuntime.Procs; + +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +using System; + +namespace Content.Tests { + sealed class DummyOpenDreamGameTiming : IOpenDreamGameTiming { + + [Dependency] IGameTiming _gameTiming = null!; + + public GameTick CurTick { get; set; } + + public TimeSpan LastTick => _gameTiming.LastTick; + + public TimeSpan RealTime => _gameTiming.RealTime; + + public TimeSpan TickPeriod => _gameTiming.TickPeriod; + + public byte TickRate { + get => _gameTiming.TickRate; + set => _gameTiming.TickRate = value; + } + } +} diff --git a/OpenDreamClient/Rendering/ClientImagesSystem.cs b/OpenDreamClient/Rendering/ClientImagesSystem.cs index 81d702ff9c..323ff80b1c 100644 --- a/OpenDreamClient/Rendering/ClientImagesSystem.cs +++ b/OpenDreamClient/Rendering/ClientImagesSystem.cs @@ -6,7 +6,6 @@ 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(); diff --git a/OpenDreamRuntime/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 744eb192a3..0f474a7b8b 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -27,7 +27,7 @@ public sealed partial class DreamManager { [Dependency] private readonly ProcScheduler _procScheduler = default!; [Dependency] private readonly DreamResourceManager _dreamResourceManager = default!; [Dependency] private readonly ITaskManager _taskManager = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IOpenDreamGameTiming _gameTiming = default!; [Dependency] private readonly DreamObjectTree _objectTree = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs b/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs index 0ac9914d47..f36e5551ca 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectWorld.cs @@ -1,5 +1,7 @@ using System.Net; using System.Net.Sockets; + +using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.Native; using OpenDreamRuntime.Resources; using OpenDreamShared; @@ -8,7 +10,6 @@ using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.Network; -using Robust.Shared.Timing; namespace OpenDreamRuntime.Objects.Types; @@ -16,7 +17,7 @@ public sealed class DreamObjectWorld : DreamObject { public override bool ShouldCallNew => false; // Gets called manually later [Dependency] private readonly IBaseServer _server = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IOpenDreamGameTiming _gameTiming = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; diff --git a/OpenDreamRuntime/Procs/IOpenDreamGameTiming.cs b/OpenDreamRuntime/Procs/IOpenDreamGameTiming.cs new file mode 100644 index 0000000000..3a2fd94dae --- /dev/null +++ b/OpenDreamRuntime/Procs/IOpenDreamGameTiming.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Timing; + +namespace OpenDreamRuntime.Procs { + public interface IOpenDreamGameTiming { + public GameTick CurTick { get; } + + public TimeSpan LastTick { get; } + + public TimeSpan RealTime { get; } + + public TimeSpan TickPeriod { get; } + + public byte TickRate { get; set; } + } +} diff --git a/OpenDreamRuntime/Procs/OpenDreamGameTiming.cs b/OpenDreamRuntime/Procs/OpenDreamGameTiming.cs new file mode 100644 index 0000000000..27f7778454 --- /dev/null +++ b/OpenDreamRuntime/Procs/OpenDreamGameTiming.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Timing; + +namespace OpenDreamRuntime.Procs { + sealed class OpenDreamGameTiming : IOpenDreamGameTiming { + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public GameTick CurTick => _gameTiming.CurTick; + + public TimeSpan LastTick => _gameTiming.LastTick; + + public TimeSpan RealTime => _gameTiming.RealTime; + + public TimeSpan TickPeriod => _gameTiming.TickPeriod; + + public byte TickRate { + get => _gameTiming.TickRate; + set => _gameTiming.TickRate = value; + } + } +} diff --git a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs index de2382e832..3abbda9718 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.Delays.cs @@ -6,7 +6,7 @@ namespace OpenDreamRuntime.Procs; // Handles delay processing for sleep() and spawn(). public sealed partial class ProcScheduler { - [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IOpenDreamGameTiming _gameTiming = default!; private PriorityQueue _tickers = new(); diff --git a/OpenDreamRuntime/ServerContentIoC.cs b/OpenDreamRuntime/ServerContentIoC.cs index 77ec1706bf..65c95d26d8 100644 --- a/OpenDreamRuntime/ServerContentIoC.cs +++ b/OpenDreamRuntime/ServerContentIoC.cs @@ -22,6 +22,7 @@ public static void Register(bool unitTests = false) { if (!unitTests) { // Unit tests use their own version IoCManager.Register(); + IoCManager.Register(); } } } From dfc4de4540f1295dbcbdb4e98805870fe01611dd Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 11 Feb 2024 11:55:24 -0500 Subject: [PATCH 09/11] Correct tick timing for DMTests --- Content.Tests/DMTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Content.Tests/DMTests.cs b/Content.Tests/DMTests.cs index 200235c47e..65aba73ae3 100644 --- a/Content.Tests/DMTests.cs +++ b/Content.Tests/DMTests.cs @@ -140,12 +140,11 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) { // Tick until our inner call has finished while (!callTask.IsCompleted || _procScheduler.HasProcsQueued || _procScheduler.HasProcsSleeping) { + GameTiming.CurTick = new GameTick(_gameTiming.CurTick.Value + 1); _dreamMan.Update(); _taskManager.ProcessPendingTasks(); - GameTiming.CurTick = new GameTick(_gameTiming.CurTick.Value + 1); - - if (watch.Elapsed.TotalSeconds > 5) { + if (GameTiming.CurTick.Value > 50000) { Assert.Fail("Test timed out"); } } From 591eb09138cd47a81d9ee78fac29f2ca62e22f80 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 11 Feb 2024 12:24:28 -0500 Subject: [PATCH 10/11] Add a failing test --- .../DMProject/Tests/Sleeping/Basic.dm | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Content.Tests/DMProject/Tests/Sleeping/Basic.dm diff --git a/Content.Tests/DMProject/Tests/Sleeping/Basic.dm b/Content.Tests/DMProject/Tests/Sleeping/Basic.dm new file mode 100644 index 0000000000..9832857dea --- /dev/null +++ b/Content.Tests/DMProject/Tests/Sleeping/Basic.dm @@ -0,0 +1,20 @@ +// RETURN TRUE + +var/should_be_set = FALSE + +/proc/RunTest() + ASSERT(world.time == 0) + sleep(world.tick_lag) + ASSERT(world.time == 1) + sleep(10) + ASSERT(world.time == 11) + StackCheck() + ASSERT(world.time == 61) + ASSERT(should_be_set) + return TRUE + +/proc/StackCheck() + ASSERT(world.time == 11) + sleep(50) + ASSERT(world.time == 61) + should_be_set = TRUE From cb18a4ee4f50623556cf9238544d3f03fc780063 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 11 Feb 2024 12:24:43 -0500 Subject: [PATCH 11/11] Fix ProcStatus returned from SleepState --- OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 3ddae174a3..397caccf73 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1784,7 +1784,7 @@ public override ProcStatus Resume() { throw exception; } - return ProcStatus.Returned; + return ProcStatus.Continue; } return Thread.HandleDefer();