Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0e13edb
Add YieldOrder tests
Cyberboss Nov 26, 2023
4f0b534
Fix yield scheduling
Cyberboss Nov 26, 2023
0911a5e
Fix ProcScheduler state poisoning between tests
Cyberboss Nov 27, 2023
f0e4f27
Up DMTests timeout x10 for slow Windows runners
Cyberboss Nov 27, 2023
86fb394
Redo sleeping as an opcode
Cyberboss Jan 1, 2024
8d7ebeb
Emit constant folded `BackgroundSleep` opcode on negative values othe…
Cyberboss Jan 1, 2024
8c66a1d
Merge master
Cyberboss Feb 3, 2024
1e4dd81
Clear ProcScheduler state each test run
Cyberboss Feb 9, 2024
7dce998
Merge remote-tracking branch 'upstream/master' into 1262-SmartYield
Cyberboss Feb 9, 2024
38e1ff4
Create `IOpenDreamGameTiming`
Cyberboss Feb 10, 2024
e3cecd2
Merge remote-tracking branch 'upstream/master' into 1262-SmartYield
Cyberboss Feb 11, 2024
dfc4de4
Correct tick timing for DMTests
Cyberboss Feb 11, 2024
591eb09
Add a failing test
Cyberboss Feb 11, 2024
cb18a4e
Fix ProcStatus returned from SleepState
Cyberboss Feb 11, 2024
eebf85b
Merge remote-tracking branch 'upstream/master' into 1262-SmartYield
Cyberboss Mar 29, 2024
447db24
attempt at continuing work from @Cyberboss #1539
Ruzihm Dec 18, 2025
29dce91
Merge remote-tracking branch 'cyberboss/1262-SmartYield' into sleepin…
Ruzihm Dec 18, 2025
20e439f
implements pseudo scheduling by doing a same-tick yield when 10 negat…
Ruzihm Dec 18, 2025
16e02b5
Merge remote-tracking branch 'upstream/master' into sleeping-negative…
Ruzihm Dec 18, 2025
62e8eb5
track threads and procs for negative sleep interruption timing
Ruzihm Dec 19, 2025
6ae008e
add back the background attribute check
Ruzihm Dec 20, 2025
b05a0aa
update delay comment
Ruzihm Dec 20, 2025
25f4801
adds support for certain statements (so far only sleep) as or increm…
Ruzihm Dec 21, 2025
de96c6a
remove requirement for sleep delay expression
Ruzihm Dec 21, 2025
3a636bd
Fixes disassembly view of sleep, remove accidental using
Ruzihm Dec 21, 2025
881b84f
lint
Ruzihm Dec 21, 2025
fb391fd
lint
Ruzihm Dec 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Content.Tests/Content.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
<DMStandard Include="..\DMCompiler\DMStandard\**" />
</ItemGroup>

<ItemGroup>
<Content Include="DMProject\Tests\Sleeping\Basic.cs" />
<Content Include="DMProject\Tests\Sleeping\Basic.dm" />
<Content Include="dmproject\tests\sleeping\YieldOrder.dm" />
</ItemGroup>

<Target Name="CopyDMStandard" AfterTargets="AfterBuild">
<Copy SourceFiles="@(DMStandard)" DestinationFiles="@(DMStandard->'$(OutDir)\DMStandard\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
Expand Down
5 changes: 2 additions & 3 deletions Content.Tests/ContentUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using OpenDreamClient;
using OpenDreamRuntime;
using OpenDreamRuntime.Map;
using OpenDreamRuntime.Procs;
using OpenDreamRuntime.Rendering;
using OpenDreamShared;
using OpenDreamShared.Rendering;
using Robust.Shared.Analyzers;
using Robust.Shared.IoC;
Expand All @@ -24,11 +24,10 @@ public class ContentUnitTest : RobustUnitTest {
protected override void OverrideIoC() {
base.OverrideIoC();

SharedOpenDreamIoC.Register();

if (Project == UnitTestProject.Server) {
ServerContentIoC.Register(unitTests: true);
IoCManager.Register<IDreamMapManager, DummyDreamMapManager>();
IoCManager.Register<IOpenDreamGameTiming, DummyOpenDreamGameTiming>();
} else if (Project == UnitTestProject.Client) {
ClientContentIoC.Register();
}
Expand Down
20 changes: 20 additions & 0 deletions Content.Tests/DMProject/Tests/Sleeping/Basic.dm
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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, 11)
sleep(-1)
ExpectOrder(6)

TestSleep(-1, 7)
ExpectOrder(8)
sleep(-1)
ExpectOrder(9)

TestSleep(1, 13)
sleep(-1)
ExpectOrder(10)
sleep(0)
ExpectOrder(12)

ASSERT(world.time == start_tick)

sleep(1)
ExpectOrder(14)

/proc/RunTest()
world.log << "Inline:"
TestSequence(MODE_INLINE)

world.log << "Background:"
TestSequence(MODE_BACKGROUND)

rand_seed(22475)
for(var/i in 1 to 10000)
world.log << "Rand-[i]:"
TestSequence(MODE_RAND)
20 changes: 18 additions & 2 deletions Content.Tests/DMTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DMCompiler.Compiler;
using NUnit.Framework;
using OpenDreamRuntime;
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Procs;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Log;
Expand All @@ -21,10 +23,14 @@ public sealed partial 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
Expand Down Expand Up @@ -81,17 +87,23 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) {
return;
}

_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}");
_dreamMan.LastDMException = null; // Nuke any exception from a prior test
_dreamMan.StartWorld();

(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)) {
Expand All @@ -107,6 +119,9 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) {
Assert.That(returned?.IsTruthy(), Is.True, "Test was expected to return TRUE");
}

var threads = _procScheduler.InspectThreads().ToList();
Assert.That(threads.Count == expectedThreads && !_procScheduler.HasProcsSleeping && !_procScheduler.HasProcsQueued, $"One or more threads did not finish!");

// Due to how softdels work we need to make sure we clean up /now/.
GC.Collect();
_dreamMan.Update();
Expand Down Expand Up @@ -137,11 +152,12 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags, int errorCode) {
watch.Start();

// Tick until our inner call has finished
while (!callTask.IsCompleted) {
while (!callTask.IsCompleted || _procScheduler.HasProcsQueued || _procScheduler.HasProcsSleeping) {
_dreamMan.Update();
_taskManager.ProcessPendingTasks();
GameTiming.CurTick = new GameTick(_gameTiming.CurTick.Value + 1);

if (watch.Elapsed.TotalMilliseconds > 500) {
if (GameTiming.CurTick.Value > 50000) {
Assert.Fail("Test timed out");
}
}
Expand Down
26 changes: 26 additions & 0 deletions Content.Tests/DummyOpenDreamGameTiming.cs
Original file line number Diff line number Diff line change
@@ -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 ushort TickRate {
get => _gameTiming.TickRate;
set => _gameTiming.TickRate = value;
}
}
}
4 changes: 4 additions & 0 deletions DMCompiler/Bytecode/DreamProcOpcode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ public enum DreamProcOpcode : byte {
NPushFloatAssign = 0x9B,
[OpcodeMetadata(0, OpcodeArgType.ArgType, OpcodeArgType.StackDelta)]
Animate = 0x9C,
[OpcodeMetadata(-1)]
Sleep = 0x9D,
[OpcodeMetadata]
BackgroundSleep = 0x9E,
}
// ReSharper restore MissingBlankLines

Expand Down
8 changes: 8 additions & 0 deletions DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public sealed class DMASTProcStatementSet(
public readonly bool WasInKeyword = wasInKeyword; // Marks whether this was a "set x in y" expression, or a "set x = y" one
}

public sealed class DMASTProcStatementSleep(
Location location,
DMASTExpression delay) : DMASTProcStatement(location) {
public DMASTExpression Delay = delay;
}

public sealed class DMASTProcStatementSpawn(Location location, DMASTExpression delay, DMASTProcBlockInner body)
: DMASTProcStatement(location) {
public DMASTExpression Delay = delay;
Expand All @@ -112,9 +118,11 @@ public sealed class DMASTProcStatementFor(
DMASTExpression? expr1,
DMASTExpression? expr2,
DMASTExpression? expr3,
DMASTProcStatement? statement3,
DMComplexValueType? dmTypes,
DMASTProcBlockInner body) : DMASTProcStatement(location) {
public DMASTExpression? Expression1 = expr1, Expression2 = expr2, Expression3 = expr3;
public DMASTProcStatement? Statement3 = statement3;
public DMComplexValueType? DMTypes = dmTypes;
public readonly DMASTProcBlockInner Body = body;
}
Expand Down
1 change: 1 addition & 0 deletions DMCompiler/Compiler/DM/DMLexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public sealed class DMLexer : TokenLexer {
{ "set", TokenType.DM_Set },
{ "call", TokenType.DM_Call },
{ "call_ext", TokenType.DM_Call},
{ "sleep", TokenType.DM_Sleep },
{ "spawn", TokenType.DM_Spawn },
{ "goto", TokenType.DM_Goto },
{ "step", TokenType.DM_Step },
Expand Down
51 changes: 42 additions & 9 deletions DMCompiler/Compiler/DM/DMParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public partial class DMParser(DMCompiler compiler, DMLexer lexer) : Parser<Token
TokenType.DM_Throw,
TokenType.DM_Null,
TokenType.DM_Switch,
TokenType.DM_Sleep,
TokenType.DM_Spawn,
TokenType.DM_Do,
TokenType.DM_While,
Expand Down Expand Up @@ -802,6 +803,7 @@ public DMASTFile File() {
TokenType.DM_Switch => Switch(),
TokenType.DM_Continue => Continue(),
TokenType.DM_Break => Break(),
TokenType.DM_Sleep => Sleep(),
TokenType.DM_Spawn => Spawn(),
TokenType.DM_While => While(),
TokenType.DM_Do => DoWhile(),
Expand Down Expand Up @@ -1124,6 +1126,22 @@ private DMASTProcStatement Set() {
return sets[0];
}

public DMASTProcStatementSleep? Sleep() {
var loc = Current().Location;

if (Check(TokenType.DM_Sleep)) {
Whitespace();
bool hasParenthesis = Check(TokenType.DM_LeftParenthesis);
Whitespace();
DMASTExpression? delay = Expression();
if (hasParenthesis) ConsumeRightParenthesis();

return new DMASTProcStatementSleep(loc, delay ?? new DMASTConstantInteger(loc, 0));
} else {
return null;
}
}

private DMASTProcStatementSpawn Spawn() {
var loc = Current().Location;
Advance();
Expand Down Expand Up @@ -1257,7 +1275,7 @@ private DMASTProcStatement For() {
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after to expression");
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.LHS, assign.RHS, endRange, step), null, null, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.LHS, assign.RHS, endRange, step), null, null, null, dmTypes, GetForBody());
} else {
Emit(WarningCode.BadExpression, "Expected = before to in for");
return new DMASTInvalidProcStatement(loc);
Expand All @@ -1272,20 +1290,20 @@ private DMASTProcStatement For() {
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2");
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, null, dmTypes, GetForBody());
}

if (!Check(ForSeparatorTypes)) {
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 1");
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, expr1, null, null, null, dmTypes, GetForBody());
}

if (Check(TokenType.DM_RightParenthesis)) {
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, expr1, null, null, null, dmTypes, GetForBody());
}

Whitespace();
Expand All @@ -1302,29 +1320,44 @@ private DMASTProcStatement For() {
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2");
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, expr1, expr2, null, null, dmTypes, GetForBody());
}

if (Check(TokenType.DM_RightParenthesis)) {
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, expr1, expr2, null, null, dmTypes, GetForBody());
}

Whitespace();
DMASTExpression? expr3 = Expression();
DMASTProcStatement? statement3 = null;
if (expr3 == null) {
CheckForStatementIncrementor();

if (Current().Type != TokenType.DM_RightParenthesis) {
Emit(WarningCode.BadExpression, "Expected 3nd expression in for");
}

expr3 = new DMASTConstantNull(loc);
}

Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 3");
ExtraColonPeriod();

return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody());
return new DMASTProcStatementFor(loc, expr1, expr2, expr3, statement3, dmTypes, GetForBody());

void CheckForStatementIncrementor() {
DMASTProcStatementSleep? sleep = Sleep();
if (sleep == null) {
expr3 = new DMASTConstantNull(loc);
statement3 = sleep;
return;
}

// TODO: additional tests, e.g., animate(...)

// if no matches, null
expr3 = new DMASTConstantNull(loc);
}

DMASTProcBlockInner GetForBody() {
Whitespace();
Expand Down
1 change: 1 addition & 0 deletions DMCompiler/Compiler/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public enum TokenType : byte {
DM_Set,
DM_Slash,
DM_SlashEquals,
DM_Sleep,
DM_Spawn,
DM_Star,
DM_StarEquals,
Expand Down
Loading
Loading