diff --git a/EasyCommands.Tests/EasyCommands.Tests.csproj b/EasyCommands.Tests/EasyCommands.Tests.csproj index a614f0f..15ee39a 100644 --- a/EasyCommands.Tests/EasyCommands.Tests.csproj +++ b/EasyCommands.Tests/EasyCommands.Tests.csproj @@ -144,6 +144,7 @@ + diff --git a/EasyCommands.Tests/ScriptTests/FunctionalTests/BlockHandlers/ThreadBlockTests.cs b/EasyCommands.Tests/ScriptTests/FunctionalTests/BlockHandlers/ThreadBlockTests.cs new file mode 100644 index 0000000..166c08b --- /dev/null +++ b/EasyCommands.Tests/ScriptTests/FunctionalTests/BlockHandlers/ThreadBlockTests.cs @@ -0,0 +1,276 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Moq; +using Sandbox.ModAPI.Ingame; +using static EasyCommands.Tests.ScriptTests.MockEntityUtility; + +namespace EasyCommands.Tests.ScriptTests { + [TestClass] + public class ThreadBlockTests { + + [TestMethod] + public void GetCurrentThreadNameWhenNotCustom() { + using (ScriptTest test = new ScriptTest(@"print the current thread name")) { + test.program.logLevel = IngameScript.Program.LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("main", test.Logger[0]); + } + } + + [TestMethod] + public void GetCurrentThreadNameWhenCustom() { + using (ScriptTest test = new ScriptTest(@" +set the current thread name to ""Hello World!"" +print the current thread name +")) { + test.program.logLevel = IngameScript.Program.LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("Hello World!", test.Logger[0]); + } + } + + [TestMethod] + public void GetCurrentThreadNameForAnonymousAsyncThreadWhenNotCustom() { + using (ScriptTest test = new ScriptTest(@" +async + print the current thread name +wait 1 +")) { + test.program.logLevel = IngameScript.Program.LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("Unknown", test.Logger[0]); + } + } + + [TestMethod] + public void GetCurrentThreadNameForAsyncThreadWhenNotCustom() { + using (ScriptTest test = new ScriptTest(@" +async doThing +wait 1 + +:doThing +print the current thread name +")) { + test.program.logLevel = IngameScript.Program.LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("doThing", test.Logger[0]); + } + } + + [TestMethod] + public void GetCurrentThreadNameForAsyncThreadWhenCustom() { + using (ScriptTest test = new ScriptTest(@" +async + set the current thread name to ""Hello World!"" + print the current thread name +wait 1 +")) { + test.program.logLevel = IngameScript.Program.LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("Hello World!", test.Logger[0]); + } + } + + [TestMethod] + public void GetAllThreadNames() { + using (ScriptTest test = new ScriptTest(@" + +queue doThing +async + set the current thread name to ""Async Thread"" + wait 0.25 +async + set the current thread name to ""Async Thread 2"" + wait 1 +set the current thread name to ""My Main"" +wait +print all thread names + +:doThing +print all thread names +wait 0.5 +print all thread names +")) { + test.RunUntilDone(); + + Assert.AreEqual(3, test.Logger.Count); + Assert.AreEqual("[\"Async Thread\",\"Async Thread 2\",doThing,\"My Main\"]", test.Logger[0]); + Assert.AreEqual("[\"Async Thread\",\"Async Thread 2\",doThing]", test.Logger[1]); + Assert.AreEqual("[\"Async Thread 2\",doThing]", test.Logger[2]); + } + } + + [TestMethod] + public void GetAsyncThreadNames() { + using (ScriptTest test = new ScriptTest(@" +queue doThing +print the list of async threads +async + set the current thread name to ""Async Thread"" + wait 0.25 +print the list of async threads +async + set the current thread name to ""Async Thread 2"" + wait 1 +print the list of async threads + +:doThing +print the list of async threads +wait 0.5 +print the list of async threads +")) { + test.RunUntilDone(); + + Assert.AreEqual(5, test.Logger.Count); + Assert.AreEqual("[]", test.Logger[0]); + Assert.AreEqual("[Unknown]", test.Logger[1]); + Assert.AreEqual("[Unknown,Unknown]", test.Logger[2]); + Assert.AreEqual("[\"Async Thread\",\"Async Thread 2\"]", test.Logger[3]); + Assert.AreEqual("[\"Async Thread 2\"]", test.Logger[4]); + } + } + + [TestMethod] + public void GetChildThreadNames() { + using (ScriptTest test = new ScriptTest(@" +print the list of child threads +async + set the current thread name to ""Async Thread"" + async + set the current thread name to ""Async Thread 2"" + wait 1 + print the list of child threads + wait 0.25 + print the list of child threads +wait 2 ticks +print the list of child threads +")) { + test.RunUntilDone(); + + Assert.AreEqual(4, test.Logger.Count); + Assert.AreEqual("[]", test.Logger[0]); + Assert.AreEqual("[\"Async Thread\"]", test.Logger[1]); + Assert.AreEqual("[\"Async Thread 2\"]", test.Logger[2]); + Assert.AreEqual("[]", test.Logger[3]); + } + } + + [TestMethod] + public void GetQueuedThreadNames() { + using (ScriptTest test = new ScriptTest(@" +print the list of queued threads +queue doThing +print the list of queued threads +queue doThing2 +print the list of queued threads + +:doThing +print the list of queued threads + +:doThing2 +print the list of queued threads +")) { + test.RunUntilDone(); + + Assert.AreEqual(5, test.Logger.Count); + Assert.AreEqual("[]", test.Logger[0]); + Assert.AreEqual("[doThing]", test.Logger[1]); + Assert.AreEqual("[doThing,doThing2]", test.Logger[2]); + Assert.AreEqual("[doThing2]", test.Logger[3]); + Assert.AreEqual("[]", test.Logger[4]); + } + } + + [TestMethod] + public void TerminateCurrentThread() { + using (ScriptTest test = new ScriptTest(@" +Print ""Beginning Thread"" +terminate the current thread +print ""Done"" +")) { + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("Beginning Thread", test.Logger[0]); + } + } + + [TestMethod] + public void TerminateQueuedThreadsDoesNotTermiateCurrentThread() { + using (ScriptTest test = new ScriptTest(@" +Print ""Calling Queue"" +callQueue 4 times +terminate all queued threads +wait 1 +print ""Done"" + +:callQueue +queue + print ""You cant kill me"" +")) { + test.RunUntilDone(); + + Assert.AreEqual(2, test.Logger.Count); + Assert.AreEqual("Calling Queue", test.Logger[0]); + Assert.AreEqual("Done", test.Logger[1]); + } + } + + [TestMethod] + public void TerminateAsynchronousThreads() { + using (ScriptTest test = new ScriptTest(@" +Print ""Calling Async"" +callAsync 4 times +terminate all async threads +print ""Done"" + +:callAsync +async + wait 1 + print ""You cant kill me"" +")) { + test.RunUntilDone(); + + Assert.AreEqual(2, test.Logger.Count); + Assert.AreEqual("Calling Async", test.Logger[0]); + Assert.AreEqual("Done", test.Logger[1]); + } + } + + [TestMethod] + public void TerminateChildThreadsDoesNotTerminateOtherAsyncThreads() { + using (ScriptTest test = new ScriptTest(@" +Print ""Calling Async"" +callAsync +wait 0.25 +cancel all child threads +print ""Done"" + +:callAsync +async + async + wait 1 + print ""This One Remains"" + wait 1 + print ""You cant kill me"" +")) { + test.RunUntilDone(); + + Assert.AreEqual(3, test.Logger.Count); + Assert.AreEqual("Calling Async", test.Logger[0]); + Assert.AreEqual("Done", test.Logger[1]); + Assert.AreEqual("This One Remains", test.Logger[2]); + } + } + } +} diff --git a/EasyCommands.Tests/ScriptTests/FunctionalTests/MultiThreadingTests.cs b/EasyCommands.Tests/ScriptTests/FunctionalTests/MultiThreadingTests.cs index 551a5ed..36f7ee8 100644 --- a/EasyCommands.Tests/ScriptTests/FunctionalTests/MultiThreadingTests.cs +++ b/EasyCommands.Tests/ScriptTests/FunctionalTests/MultiThreadingTests.cs @@ -1,5 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; +using System.Linq; using static IngameScript.Program; namespace EasyCommands.Tests.ScriptTests { @@ -339,5 +341,233 @@ async call runAsync } } + [TestMethod] + public void asyncVariableIsNotAffectedByMainThread() { + String script = @" +:main +set a to 1 +async call runAsync +set a to 2 + +:runAsync +print 'Variable is: ' + a +wait 1 +print 'Variable is: ' + a +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(2, test.Logger.Count); + Assert.AreEqual("Variable is: 1", test.Logger[0]); + Assert.AreEqual("Variable is: 1", test.Logger[1]); + } + } + + [TestMethod] + public void awaitCommandExecutesButBlocksOnAsyncThreads() { + String script = @" +:main +await + async + Print ""Async Thread"" + wait 1 + Print ""Async Thread Done"" + print ""Main Thread"" + async + Print ""Async Thread 2"" + wait 0.5 + Print ""Async Thread 2 Done"" +print ""Main Thread Done"" +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(6, test.Logger.Count); + Assert.AreEqual("Main Thread", test.Logger[0]); + Assert.AreEqual("Async Thread", test.Logger[1]); + Assert.AreEqual("Async Thread 2", test.Logger[2]); + Assert.AreEqual("Async Thread 2 Done", test.Logger[3]); + Assert.AreEqual("Async Thread Done", test.Logger[4]); + Assert.AreEqual("Main Thread Done", test.Logger[5]); + } + } + + [TestMethod] + public void awaitCommandWaitsForAsyncThreadSpawnedInSubFunction() { + String script = @" +:main +await + callAsync ""Async Thread"" 1 + print ""Main Thread"" + callAsync ""Async Thread 2"" 0.5 +print ""Main Thread Done"" + +:callAsync threadName timeout +async + Print threadName + wait timeout + Print threadName + "" Done"" +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(6, test.Logger.Count); + Assert.AreEqual("Main Thread", test.Logger[0]); + Assert.AreEqual("Async Thread", test.Logger[1]); + Assert.AreEqual("Async Thread 2", test.Logger[2]); + Assert.AreEqual("Async Thread 2 Done", test.Logger[3]); + Assert.AreEqual("Async Thread Done", test.Logger[4]); + Assert.AreEqual("Main Thread Done", test.Logger[5]); + } + } + + [TestMethod] + public void nestedAwaitCommandsProperlyBlocks() { + String script = @" +:main +await + callAsync ""Async Thread 2"" 1 + await + callAsync ""Async Thread"" 0.5 + print ""Main Thread"" +print ""Main Thread Done"" + +:callAsync threadName timeout +async + Print threadName + wait timeout + Print threadName + "" Done"" +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(6, test.Logger.Count); + Assert.AreEqual("Async Thread 2", test.Logger[0]); + Assert.AreEqual("Async Thread", test.Logger[1]); + Assert.AreEqual("Async Thread Done", test.Logger[2]); + Assert.AreEqual("Main Thread", test.Logger[3]); + Assert.AreEqual("Async Thread 2 Done", test.Logger[4]); + Assert.AreEqual("Main Thread Done", test.Logger[5]); + } + } + + [TestMethod] + public void awaitCommandCanBeBrokenOutOf() { + String script = @" +:main +await + print ""Main Thread"" + async + Print ""Async Thread"" + wait 1 + Print ""Async Thread Done"" + wait 0.5 + break +print ""Main Thread Done"" + +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(4, test.Logger.Count); + Assert.AreEqual("Main Thread", test.Logger[0]); + Assert.AreEqual("Async Thread", test.Logger[1]); + Assert.AreEqual("Main Thread Done", test.Logger[2]); + Assert.AreEqual("Async Thread Done", test.Logger[3]); + } + } + + [TestMethod] + public void awaitCommandContinueActsAsBreak() { + String script = @" +:main +await + print ""Main Thread"" + async + Print ""Async Thread"" + wait 1 + Print ""Async Thread Done"" + wait 0.5 + continue +print ""Main Thread Done"" + +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(4, test.Logger.Count); + Assert.AreEqual("Main Thread", test.Logger[0]); + Assert.AreEqual("Async Thread", test.Logger[1]); + Assert.AreEqual("Main Thread Done", test.Logger[2]); + Assert.AreEqual("Async Thread Done", test.Logger[3]); + } + } + + [TestMethod] + public void awaitCommandResetsCorrectly() { + String script = @" +:main +await + async + Print ""Async Thread"" + Print ""Async Thread Done"" +print ""Main Thread Done"" +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(3, test.Logger.Count); + Assert.AreEqual("Async Thread", test.Logger[0]); + Assert.AreEqual("Async Thread Done", test.Logger[1]); + Assert.AreEqual("Main Thread Done", test.Logger[2]); + + test.Logger.Clear(); + test.RunUntilDone(); + + Assert.AreEqual(3, test.Logger.Count); + Assert.AreEqual("Async Thread", test.Logger[0]); + Assert.AreEqual("Async Thread Done", test.Logger[1]); + Assert.AreEqual("Main Thread Done", test.Logger[2]); + } + } + + + [TestMethod] + public void terminateAllThreadsFromAsyncThread() { + String script = @" +await + async + set the current thread name to ""My Async Thread"" + wait 1.25 + print ""Terminating Threads!"" + terminate all threads + wait 1 +print ""Done!"" +"; + + using (var test = new ScriptTest(script)) { + test.program.logLevel = LogLevel.SCRIPT_ONLY; + test.RunUntilDone(); + + Assert.AreEqual(1, test.Logger.Count); + Assert.AreEqual("Terminating Threads!", test.Logger[0]); + } + } + + } } diff --git a/EasyCommands/BlockHandlers/BlockHandlerRegistry.cs b/EasyCommands/BlockHandlers/BlockHandlerRegistry.cs index 4e3fe53..a154813 100644 --- a/EasyCommands/BlockHandlers/BlockHandlerRegistry.cs +++ b/EasyCommands/BlockHandlers/BlockHandlerRegistry.cs @@ -76,6 +76,7 @@ public static class BlockHandlerRegistry { { Block.TANK, new GasTankBlockHandler() }, { Block.TERMINAL, new TerminalBlockHandler() }, { Block.TIMER, new TimerBlockHandler() }, + { Block.THREAD, new ThreadBlockHandler() }, { Block.THRUSTER, new ThrusterBlockHandler()}, { Block.TURBINE, new EngineBlockHandler("WindTurbine") }, { Block.TURRET, new TurretBlockHandler()}, @@ -95,6 +96,14 @@ public static List GetSelf(Block? blockType) => : null; public static List GetBlocks(Block blockType, string selector = null) { + if (blockType == Block.THREAD) + return blockHandlers[blockType].SelectBlocks(GetActiveThreads(), t => selector?.Equals(t.customName ?? t.name) ?? true) + .Select(t => ((Thread)t).originalThread) + .Distinct() + .OrderBy(t => t == PROGRAM.currentThread) + .OfType() + .ToList(); + if (PROGRAM.blockCache.Count == 0) PROGRAM.GridTerminalSystem.GetBlocks(PROGRAM.blockCache); @@ -108,6 +117,23 @@ public static List GetBlocksInGroup(Block blockType, String groupName) = PROGRAM.GridTerminalSystem.GetBlockGroupWithName(s)?.GetBlocks(blocks); return blockHandlers[blockType].SelectBlocks(blocks); }); + + static List GetActiveThreads() { + var asyncThreads = PROGRAM.asyncThreadQueue.Select(t => t.WithName("async")).ToList(); + var queuedThreads = PROGRAM.threadQueue.Skip(1).Select(t => t.WithName("queued")).ToList(); + var currentThread = PROGRAM.currentThread.WithName("current"); + var programCurrentThread = PROGRAM.currentThread; + var childrenThreads = PROGRAM.asyncThreadQueue.Where(t => PROGRAM.currentThread == t.parentThread).Select(t => t.WithName("child")).ToList(); + + return Combine( + Once(currentThread), + PROGRAM.asyncThreadQueue, + PROGRAM.threadQueue, + asyncThreads, + queuedThreads, + childrenThreads + ).ToList(); + } } } } diff --git a/EasyCommands/BlockHandlers/BlockHandlers.cs b/EasyCommands/BlockHandlers/BlockHandlers.cs index e5d3117..e6d10d4 100644 --- a/EasyCommands/BlockHandlers/BlockHandlers.cs +++ b/EasyCommands/BlockHandlers/BlockHandlers.cs @@ -87,7 +87,7 @@ public interface IBlockHandler { PropertySupplier GetDefaultProperty(Return type); PropertySupplier GetDefaultProperty(Direction direction); Direction GetDefaultDirection(); - List SelectBlocks(List blocks, Func selector = null) where U : IMyTerminalBlock; + List SelectBlocks(List blocks, Func selector = null); String GetName(Object block); Primitive GetPropertyValue(Object block, PropertySupplier property); @@ -118,10 +118,12 @@ public virtual PropertyHandler GetPropertyHandler(PropertySupplier property) public string GetPropertyHash(PropertySupplier propertySupplier) => propertySupplier.propertyValues.OrderBy(p => p.propertyType).Aggregate("", (a, b) => a + b.propertyType); - public List SelectBlocks(List blocks, Func selector = null) where U : IMyTerminalBlock => + public List SelectBlocks(List blocks, Func selector = null) => SelectBlocksByType(blocks, selector).OfType().ToList(); - public abstract IEnumerable SelectBlocksByType(List blocks, Func selector = null) where U : IMyTerminalBlock; + public virtual IEnumerable SelectBlocksByType(List blocks, Func selector = null) => + blocks.Where(selector ?? (b => true)).OfType(); + public abstract string Name(T block); public Direction GetDefaultDirection() => defaultDirection; @@ -212,7 +214,7 @@ PropertyHandler TypedHandler(U defaultType, Func Reso public abstract class MultiInstanceBlockHandler : BlockHandler where T : class { public override IEnumerable SelectBlocksByType(List blocks, Func selector = null) => - blocks.Where(selector ?? (b => true)).SelectMany(b => GetInstances(b)); + blocks.Where(selector ?? (b => true)).SelectMany(b => GetInstances((IMyTerminalBlock)b)); public abstract IEnumerable GetInstances(IMyTerminalBlock block); } } diff --git a/EasyCommands/BlockHandlers/GridBlockHandlers.cs b/EasyCommands/BlockHandlers/GridBlockHandlers.cs index 3b0e15b..e6de0bf 100644 --- a/EasyCommands/BlockHandlers/GridBlockHandlers.cs +++ b/EasyCommands/BlockHandlers/GridBlockHandlers.cs @@ -28,7 +28,7 @@ public GridBlockHandler() { public override string Name(IMyCubeGrid block) => block.CustomName; public override IEnumerable SelectBlocksByType(List blocks, Func selector = null) => - blocks.Where(selector ?? (b => true)).Select(b => b.CubeGrid).Distinct(); + blocks.Where(selector ?? (b => true)).Select(b => ((IMyTerminalBlock)b).CubeGrid).Distinct(); } } } diff --git a/EasyCommands/BlockHandlers/TerminalBlockHandlers.cs b/EasyCommands/BlockHandlers/TerminalBlockHandlers.cs index 4a52a30..10bff9f 100644 --- a/EasyCommands/BlockHandlers/TerminalBlockHandlers.cs +++ b/EasyCommands/BlockHandlers/TerminalBlockHandlers.cs @@ -114,9 +114,6 @@ public TerminalBlockHandler() { float MaxIntegrity(IMySlimBlock block) => block.MaxIntegrity; float BuildRatio(IMySlimBlock block) => BuildIntegrity(block) / MaxIntegrity(block); - public override IEnumerable SelectBlocksByType(List blocks, Func selector = null) => - blocks.Where(selector ?? (b => true)).OfType(); - public override PropertyHandler GetPropertyHandler(PropertySupplier property) { try { return base.GetPropertyHandler(property); diff --git a/EasyCommands/BlockHandlers/ThreadBlockHandlers.cs b/EasyCommands/BlockHandlers/ThreadBlockHandlers.cs new file mode 100644 index 0000000..f4ce6aa --- /dev/null +++ b/EasyCommands/BlockHandlers/ThreadBlockHandlers.cs @@ -0,0 +1,32 @@ +using Sandbox.Game.EntityComponents; +using Sandbox.ModAPI.Ingame; +using Sandbox.ModAPI.Interfaces; +using SpaceEngineers.Game.ModAPI.Ingame; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using VRage; +using VRage.Collections; +using VRage.Game; +using VRage.Game.Components; +using VRage.Game.GUI.TextPanel; +using VRage.Game.ModAPI.Ingame; +using VRage.Game.ModAPI.Ingame.Utilities; +using VRage.Game.ObjectBuilders.Definitions; +using VRageMath; + +namespace IngameScript { + partial class Program { + public class ThreadBlockHandler : BlockHandler { + public ThreadBlockHandler() { + AddStringHandler(Property.NAME, t => t.customName ?? t.name, (t,s) => t.customName = s); + AddBooleanHandler(Property.COMPLETE, t => false, (t, b) => { t.Command = new NullCommand(); if(t==PROGRAM.currentThread) throw new ThreadInterruptException(); }); + defaultPropertiesByPrimitive[Return.BOOLEAN] = Property.COMPLETE; + } + + public override string Name(Thread thread) => thread.customName ?? thread.name; + } + } +} diff --git a/EasyCommands/CommandParsers/CommandParameters.cs b/EasyCommands/CommandParsers/CommandParameters.cs index cba2b7d..41e8244 100644 --- a/EasyCommands/CommandParsers/CommandParameters.cs +++ b/EasyCommands/CommandParsers/CommandParameters.cs @@ -56,6 +56,7 @@ public class RoundCommandParameter : SimpleCommandParameter { } public class CastCommandParameter : SimpleCommandParameter { } public class RelativeCommandParameter : SimpleCommandParameter { } public class CommandSeparatorCommandParameter : SimpleCommandParameter { } + public class AwaitCommandParameter : SimpleCommandParameter { } public abstract class ValueCommandParameter : SimpleCommandParameter { public T value; diff --git a/EasyCommands/CommandParsers/ParameterParsers.cs b/EasyCommands/CommandParsers/ParameterParsers.cs index e05d57f..0966973 100644 --- a/EasyCommands/CommandParsers/ParameterParsers.cs +++ b/EasyCommands/CommandParsers/ParameterParsers.cs @@ -206,7 +206,8 @@ public void InitializeParsers() { AddWords(Words("send"), new SendCommandParameter()); AddWords(Words("print", "log", "echo", "write"), new PrintCommandParameter()); AddWords(Words("queue", "schedule"), new QueueCommandParameter(false)); - AddWords(Words("async", "parallel"), new QueueCommandParameter(true)); + AddAmbiguousWords(Words("async", "parallel"), new QueueCommandParameter(true)); + AddWords(Words("await", "blocking"), new AwaitCommandParameter()); AddWords(Words("transfer", "give"), new TransferCommandParameter(true)); AddWords(Words("take"), new TransferCommandParameter(false)); AddWords(Words("->"), new KeyedVariableCommandParameter()); @@ -327,7 +328,7 @@ public void InitializeParsers() { return false; }); AddControlWords(Words("return"), thread => { - FunctionCommand currentFunction = thread.GetCurrentCommand(command => true); + FunctionCommand currentFunction = thread.GetCurrentCommand(); Command nullCommand = new NullCommand(); if (currentFunction == null) thread.Command = nullCommand; else currentFunction.function = nullCommand; @@ -390,6 +391,7 @@ public void InitializeParsers() { AddBlockWords(Words("searchlight"), Block.SEARCHLIGHT); AddBlockWords(Words("turretcontroller"), Block.TURRET_CONTROLLER); AddBlockWords(Words(), Words(), Block.GRID, PluralWords("grid")); + AddBlockWords(PluralWords("thread"), Words(), Block.THREAD); AddAliasWords(Words("can"), "is able"); AddAliasWords(Words("cannot"), "is not able"); diff --git a/EasyCommands/CommandParsers/ProcessingEngine.cs b/EasyCommands/CommandParsers/ProcessingEngine.cs index 9c30a19..9fd8acd 100644 --- a/EasyCommands/CommandParsers/ProcessingEngine.cs +++ b/EasyCommands/CommandParsers/ProcessingEngine.cs @@ -376,7 +376,11 @@ partial class Program { //QueueProcessor OneValueRule(Type, requiredRight(), - (p, command) => new CommandReferenceParameter(new QueueCommand(command.value, p.value))), + (p, command) => new CommandReferenceParameter(new QueueCommand { command = command.value, async = p.value })), + + //AwaitProcessor + OneValueRule(Type, requiredRight(), + (p, command) => new CommandReferenceParameter(new AwaitCommand { subCommands = command.value })), //IteratorProcessor ThreeValueRule(Type, requiredRight(), requiredRight(), requiredEither(), diff --git a/EasyCommands/Commands/Commands.cs b/EasyCommands/Commands/Commands.cs index d0c8219..093241a 100644 --- a/EasyCommands/Commands/Commands.cs +++ b/EasyCommands/Commands/Commands.cs @@ -35,18 +35,15 @@ public virtual void Reset() { } public class QueueCommand : Command { public Command command; - bool async; - - public QueueCommand(Command Command, bool Async) { - command = Command; - async = Async; - } + public bool async; public override bool Execute() { Thread thread; if (async) { thread = new Thread(command.Clone(), "Async", "Unknown"); + thread.parentThread = PROGRAM.currentThread; PROGRAM.QueueAsyncThread(thread); + PROGRAM.currentThread.GetCurrentCommand()?.blockingThreads?.Add(thread); } else { thread = new Thread(command.Clone(), "Queued", "Unknown"); PROGRAM.QueueThread(thread); @@ -193,6 +190,37 @@ public class ControlCommand : Command { public override bool Execute() => controlFunction(PROGRAM.currentThread); } + public class AwaitCommand : Command, IInterruptableCommand { + public List blockingThreads = NewList(); + public Command subCommands; + bool completedSubCommands = false; + + public override bool Execute() => + (completedSubCommands = completedSubCommands || subCommands.Execute()) + && !PROGRAM.asyncThreadQueue.Intersect(blockingThreads).Any(); + + public override Command SearchCurrentCommand(Func filter) { + return subCommands.SearchCurrentCommand(filter) ?? base.SearchCurrentCommand(filter); + } + + public override Command Clone() => new AwaitCommand { subCommands = subCommands}; + + public override void Reset() { + blockingThreads.Clear(); + subCommands.Reset(); + completedSubCommands = false; + } + + public void Break() { + Reset(); + completedSubCommands = true; + } + + public void Continue() { + Break(); + } + } + public class WaitCommand : Command { public IVariable waitInterval; double remainingWaitTime = -1; diff --git a/EasyCommands/Common/Exceptions.cs b/EasyCommands/Common/Exceptions.cs index fd5d402..fcccb25 100644 --- a/EasyCommands/Common/Exceptions.cs +++ b/EasyCommands/Common/Exceptions.cs @@ -27,6 +27,8 @@ public InterruptException(ProgramState programState) { } } + public class ThreadInterruptException : Exception { } + public class ParserException : Exception { public ParserException(string msg) : base(msg) { } } diff --git a/EasyCommands/Common/Types.cs b/EasyCommands/Common/Types.cs index 3d3c8fe..79990d0 100644 --- a/EasyCommands/Common/Types.cs +++ b/EasyCommands/Common/Types.cs @@ -72,6 +72,7 @@ public enum Block { SUSPENSION, TANK, TERMINAL, + THREAD, THRUSTER, TIMER, TURBINE, diff --git a/EasyCommands/Common/Utilities.cs b/EasyCommands/Common/Utilities.cs index eebae6e..23fc7cc 100644 --- a/EasyCommands/Common/Utilities.cs +++ b/EasyCommands/Common/Utilities.cs @@ -37,6 +37,7 @@ partial class Program { public static IEnumerable Range(int start, int count) => Enumerable.Range(start, count); public static IEnumerable Empty() => Enumerable.Empty(); public static IEnumerable Once(T element) => Enumerable.Repeat(element, 1); + public static IEnumerable Combine(params IEnumerable[] collections) => collections.Aggregate((a, b) => a.Concat(b)); //Other useful utilities public static IVariable GetStaticVariable(object o) => new StaticVariable(ResolvePrimitive(o)); diff --git a/EasyCommands/EasyCommands.csproj b/EasyCommands/EasyCommands.csproj index 8ce367d..566fcbd 100644 --- a/EasyCommands/EasyCommands.csproj +++ b/EasyCommands/EasyCommands.csproj @@ -128,6 +128,7 @@ + diff --git a/EasyCommands/Program.cs b/EasyCommands/Program.cs index ce4a549..26c4b40 100644 --- a/EasyCommands/Program.cs +++ b/EasyCommands/Program.cs @@ -41,7 +41,8 @@ public partial class Program : MyGridProgram { public ProgramState state = ProgramState.STOPPED; - public Dictionary functions = NewDictionary(); + public Dictionary functions = NewDictionary(); + public Thread currentThread; public Random randomGenerator = new Random(); @@ -165,40 +166,25 @@ void RunThreads() { selectorCache.Clear(); //If no current commands, we've been asked to restart. start at the top. - if (threadQueue.Count == 0 && asyncThreadQueue.Count == 0) { + if (threadQueue.Count + asyncThreadQueue.Count == 0) { FunctionDefinition defaultFunctionDefinition = functions[defaultFunction]; threadQueue.Add(new Thread(defaultFunctionDefinition.function.Clone(), "Main", defaultFunctionDefinition.functionName)); } - Info("Queued Threads: " + threadQueue.Count()); - Info("Async Threads: " + asyncThreadQueue.Count()); + Info("Queued Threads: " + threadQueue.Count); + Info("Async Threads: " + asyncThreadQueue.Count); //Run first command in the queue. Could be from a message, program run request, or request to start the main program. - if (threadQueue.Count > 0 ) { - currentThread = threadQueue[0]; - Info(currentThread.GetName()); - if(currentThread.Command.Execute()) { - threadQueue.RemoveAt(0); - } - } + int threadIndex = 0; + + if (threadQueue.Count > 0) RunThread(threadQueue, ref threadIndex); //Process 1 iteration of all async commands, removing from queue if processed. - int asyncThreadQueueIndex = 0; - - while (asyncThreadQueueIndex < asyncThreadQueue.Count) { - currentThread = asyncThreadQueue[asyncThreadQueueIndex]; - Info(currentThread.GetName()); - if (currentThread.Command.Execute()) { - asyncThreadQueue.RemoveAt(asyncThreadQueueIndex); - } else { - asyncThreadQueueIndex++; - } - } + threadIndex = 0; + + while (threadIndex < asyncThreadQueue.Count) RunThread(asyncThreadQueue, ref threadIndex); + + state = (threadQueue.Count + asyncThreadQueue.Count == 0) ? ProgramState.COMPLETE : ProgramState.RUNNING; - if(threadQueue.Count == 0 && asyncThreadQueue.Count == 0) { - state = ProgramState.COMPLETE; - } else { - state = ProgramState.RUNNING; - } //InterruptException is thrown by control commands to interrupt execution (stop, pause, restart). //The command itself has set the correct state of the command queues, we just need to set the program state. } catch(InterruptException interrupt) { @@ -206,6 +192,18 @@ void RunThreads() { } } + public void RunThread(List threadQueue, ref int index) { + currentThread = threadQueue[index]; + try { + Info(currentThread.GetName()); + if (currentThread.Command.Execute()) + threadQueue.RemoveAt(index); + else index++; + } catch(ThreadInterruptException) { + threadQueue.RemoveAt(index); + } + } + public void QueueRequest(String argument) { if (!String.IsNullOrEmpty(argument)) threadQueue.Insert(0, new Thread(ParseCommand(argument), "Request", argument)); } @@ -213,20 +211,24 @@ public void QueueRequest(String argument) { public class Thread { public Command Command { get; set; } public Dictionary threadVariables = NewDictionary(); - String prefix; - String name; + public string customName, name, prefix; + public Thread parentThread, originalThread; - public T GetCurrentCommand(Func filter) where T : class => - Command.SearchCurrentCommand(command => command is T && filter(command)) as T; + public T GetCurrentCommand(Func filter = null) where T : class => + Command.SearchCurrentCommand(command => command is T && (filter == null || filter(command))) as T; - public Thread(Command command, string p, string n) { + public Thread(Command command, string Prefix, string Name, string CustomName = null, Thread OriginalThread = null) { Command = command; - prefix = p; - name = n; + prefix = Prefix; + name = Name; + customName = CustomName; + originalThread = OriginalThread ?? this; } - public String GetName() => $"[{prefix}] {name}"; + public String GetName() => $"[{prefix}] {customName ?? name}"; public void SetName(String s) => name = s; + + public Thread WithName(string customName) => new Thread(Command, prefix, name, customName, this); } public class FunctionDefinition { diff --git a/docs/EasyCommands/cheatsheet.md b/docs/EasyCommands/cheatsheet.md index ede35ed..9a48445 100644 --- a/docs/EasyCommands/cheatsheet.md +++ b/docs/EasyCommands/cheatsheet.md @@ -277,6 +277,7 @@ These properties require a Variable value as part of the property * Print - ```print, log, echo, write``` * Queue - ```queue, schedule``` * Async - ```async, parallel``` +* Await - ```await, blocking``` * Transfer - ```transfer, give```(Source -> Destination) * Transfer - ```take``` (Destination -> Source) * For Each - ```each, every``` diff --git a/docs/EasyCommands/commands.md b/docs/EasyCommands/commands.md index 5d6eca8..f6a780f 100644 --- a/docs/EasyCommands/commands.md +++ b/docs/EasyCommands/commands.md @@ -605,12 +605,12 @@ print "Run away!" If you load the above script into a new EasyCommands instance, you'll notice "[Main] flee" in the list of running commands. This is because EasyCommands has completely switched execution to the flee program. -## Queue & Async Commands +## Queueing & Async Commands EasyCommands supports a concept for [Threads](https://spaceengineers.merlinofmines.com/EasyCommands/threads "Threads"), which allows you to execute multiple programs in parallel, or to queue up commands to run after the initial command is finished executing. The general structure of this command is ``` ``` -### Queuing Commands +### Queuing Command You can "queue" commands to run after we've finished running the current script. You might use this if you create a factory that builds different types of vehicles, and you want to queue up 2 of one type and 1 of another. @@ -633,7 +633,7 @@ wait 2 set my display text to "Idle..." ``` -### Async Commands +### Async Command An asynchronous command is a command that you spin off to run on its own [Thread](https://spaceengineers.merlinofmines.com/EasyCommands/threads "Threads"). Asynchronous commands run in the same tick as the main script, so effectively they run in parallel. Async commands allow a single program to run multiple scripts simultaneously so that 1 EasyCommands script can manage multiple things. This is useful and powerful, but be careful about trying to run too much in 1 script! Async commands will automatically end once they complete their task. See [Threads](https://spaceengineers.merlinofmines.com/EasyCommands/threads "Threads") for more information on how parameters are passed from the caller to the asynchronous threads and other information. @@ -661,8 +661,56 @@ Print "Running task: " + taskName repeat ``` +### Await Command +The Await Command compliments the Async command by allowing you to start one or more asynchronous [Threads](https://spaceengineers.merlinofmines.com/EasyCommands/threads "Threads") and then wait for all of them to complete before proceeding. + +Specifically, the Await Command will wait for all async commands executed within its scope to complete before proceeding to the next command. + +Keywords: ```await, blocking``` + +The most common usage for this is to run multiple sub-commands in parallel but wait for all of them to finish before proceeding. You can also continue to run commands within the calling thread while waiting for the asynchronous commands to complete. + +``` +:main +#Await will not wait for this command to complete as it is outside the scope of the await command. +async runTask1 + +await + Print "Doing some thing in the main thread" + async runTask2 + async runTask3 + Print "Doing some other thing in the main thread" +print "Done Running Async Tasks!" + +:runTask1 +#Some long running task +Print "Running Task 1" +wait 1 second + +:runTask2 +Print "Running Task 2" +wait 2 seconds + +:runTask3 +Print "Running Task 3" +wait 3 seconds +``` + +Make sure that any async commands you execute within the await command always complete or else you'll block the calling thread forever. However, you can also stop waiting for asynchronous threads using either the "break" or "continue" keywords. + +``` +await + async + wait 10 seconds + print "This Takes Forever!" + wait 2 seconds + print "Im done waiting!" + break +print "Done" +``` + ## Control Commands -As in the above example, you might find that sometimes you want to pause, stop, or restart your script +You might find that sometimes you want to pause, stop, or restart your script entirely. Stopping or Restarting a script will clear all running & asynchronous threads, and clear all local and global variables. @@ -680,8 +728,6 @@ pause ### The Repeat Command The "repeat" command is a special command often used for asynchronous threads to keep executing the given function. The Repeat command effectively restarts the current "thread", rather than the whole program. - - ``` set my display to "Tick" wait 1 @@ -690,7 +736,7 @@ wait 1 repeat ``` -If you specify a "goto " somewhere before calling "repeat", the script will repeat from the last "goto function". Note that any previous parameters passed to that function are not passed when calling repeat. +If you specify a "goto functionName" somewhere before calling "repeat", the script will repeat from the last "goto function". Note that any previous parameters passed to that function are not passed when calling repeat. ## Send/Listen/Forget Commands diff --git a/docs/EasyCommands/threads.md b/docs/EasyCommands/threads.md index b87f013..49c2c0e 100644 --- a/docs/EasyCommands/threads.md +++ b/docs/EasyCommands/threads.md @@ -76,6 +76,23 @@ Print "Managing Inventories" replay ``` +You can also spawn async threads and then wait for them all to complete: +``` +:main +await + async task1 + async task2 +Print "Done with all my tasks!" + +:task1 +wait 1 second + +:task2 +wait 2 seconds +``` + +See the [Await Command](https://spaceengineers.merlinofmines.com/EasyCommands/commands#await-command) for more information. + ## Thread Variables Each thread maintains its own set of variables, which are only accessible from commands / functions called within that thread. This allows threads to operate independently and not stomp on each other's variables. The drawback is that you can't share variables across threads, unless you explicitly make those variables global when setting them. Global variables are shared across all threads. @@ -133,4 +150,62 @@ Print "Opening " + myDoors :closeDoors Print "Closing " + myDoors +``` + +## Thread Management +You can directly interact with threads, similar to [Block Handlers](https://spaceengineers.merlinofmines.com/EasyCommands/blockHandlers), either by name or using some special keywords as described below. + +These commands will allow you to get or set thread names, and to terminate threads. Thread names appear in the Detailed Info of the Programmable Block, so it can sometimes be useful to manually set the thread name to help distinguish currently running threads. + +Special Keywords: +```all``` - all running and queued threads, including the currently running thread +```current``` - the currently running thread, which could be one of the concurrently running async threads. +```queued``` - currently queued threads, which does not include any async threads or the currently running main thread. +```async``` - all currently running async threads, which might include the currently running thread if the current thread is also an async thread. +```child``` - all async threads that were created by the currently running thread. Does not include any async threads spawned by children of this thread. + + +### Getting and Set Thread Names +You can get/set the name of the current running thread using the following commands. + +``` +#Current Thread +Print "Current Thread" + the current thread name +set the current thread name to "My Running Task" + +#Queued Threads +print the list of queued thread names + +#Async Threads +print the list of async thread names + +#Child Threads +print the list of child thread names +``` + +You can also rename other threads: +``` +set asyncThread to my async thread names +for each asyncThread in my asyncThreads + set asyncThread thread name to "Async Thread" +``` + +### Terminate Threads +You can also terminate any thread, including the currently running thread. + +``` +#Immediately terminates the current thread +terminate the current thread + +#Terminate all async threads, which may also include the currently running thread. +terminate async threads + +#Terminate queued threads, effectively clearing the thread queue. +terminate queued threads + +#Terminate async threads spawned by the current thread +terminate child threads + +#Terminate a thread by name +terminate "My Thread" thread ``` \ No newline at end of file