Completely rework the Sequencery library and abandon MechDoor in favor of just using the action sequencer script.

This commit is contained in:
2025-02-14 15:52:28 -05:00
parent cd9ee88172
commit f5d3dbc275
25 changed files with 583 additions and 586 deletions

View File

@ -0,0 +1,62 @@
// Represents a series of ActionSequences
// that are mutually exclusive. Typically
// this is designed to represent multiple ActionSequences
// that act on the same set of blocks, such as an
// "Extend" and "Retract" action for a multi-stage mechanical
// construction, or perhaps "Open" and "Close" sequences for a set of
// mechanical doors.
using System.Collections.Generic;
using System.Linq;
namespace IngameScript
{
partial class Program
{
public class ActionGroup
{
public string Name { get; private set; }
public bool Running { get; private set; } = false;
private Dictionary<string, ActionSequence> _actions = new Dictionary<string, ActionSequence>();
private IConsole _console;
public ActionGroup(IConsole console, string name)
{
// Todo: use a PrefixedConsole here?
_console = console;
Name = name;
}
// Add an action to the sequence called actionName, which will be created if it doesn't already exist.
public void AddActionBlock(string actionName, int step, IBlockAction actionBlock)
{
if (!_actions.ContainsKey(actionName)) _actions[actionName] = new ActionSequence(actionName);
_actions[actionName].Add(step, actionBlock);
}
public IEnumerator<bool> RunAction(string actionName)
{
if (Running)
{
_console.Print($"Ignoring action '{actionName}' on '{Name}'. Already running an action.");
yield break;
}
if (!_actions.ContainsKey(actionName))
{
_console.Print($"No action '{actionName}' defined for '{Name}'");
yield break;
}
Running = true;
IEnumerator<bool> job = _actions[actionName].Run();
while (job.MoveNext()) yield return true;
Running = false;
}
}
}
}

View File

@ -0,0 +1,65 @@
// An ActionSequence encapsulates a series of IBlockActions to perform.
// It represents a single logical coordinated "action". The action may have
// multiple steps, each with multiple actions. It provides a method to asynchronously
// perform those steps in sequence.
using System.Collections.Generic;
namespace IngameScript
{
partial class Program
{
public class ActionSequence
{
public string Name { get; private set; }
public bool Running { get; private set; } = false;
private SortedDictionary<int, List<IBlockAction>> _steps = new SortedDictionary<int, List<IBlockAction>>();
public ActionSequence(string name)
{
Name = name;
}
public void Add(int step, IBlockAction action)
{
if (!_steps.ContainsKey(step)) _steps[step] = new List<IBlockAction>();
_steps[step].Add(action);
}
public IEnumerator<bool> Run()
{
if (Running) yield break;
Running = true;
List<IEnumerator<bool>> jobs = new List<IEnumerator<bool>>();
foreach (List<IBlockAction> step in _steps.Values)
{
foreach (IBlockAction action in step)
{
jobs.Add(action.Run());
}
bool stepRunning = true;
while (stepRunning)
{
stepRunning = false;
foreach (IEnumerator<bool> job in jobs)
{
if (job.MoveNext()) stepRunning = true;
}
yield return true;
}
foreach (IEnumerator<bool> job in jobs)
{
job.Dispose();
}
jobs.Clear();
}
Running = false;
}
}
}
}

View File

@ -0,0 +1,52 @@
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
namespace IngameScript
{
partial class Program
{
public class BlockActionDoor : IBlockAction
{
public enum DoorAction
{
Open,
Close,
}
public bool Running { get; private set; } = false;
private IMyDoor _door;
private DoorAction _action;
private bool _lockDoor;
public BlockActionDoor(
IMyDoor door,
DoorAction action,
bool lockDoor = true
)
{
_door = door;
_action = action;
_lockDoor = lockDoor;
}
public IEnumerator<bool> Run()
{
switch (_action)
{
case DoorAction.Open:
_door.Enabled = true;
_door.OpenDoor();
while (_door.Status != DoorStatus.Open) yield return true;
break;
case DoorAction.Close:
_door.Enabled = true;
_door.CloseDoor();
while (_door.Status != DoorStatus.Closed) yield return true;
break;
}
if (_lockDoor) _door.Enabled = false;
}
}
}
}

View File

@ -0,0 +1,40 @@
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
namespace IngameScript
{
partial class Program
{
public class BlockActionPiston : IBlockAction
{
public bool Running { get; private set; } = false;
private IMyPistonBase _piston;
private float _position;
private float _velocity;
public BlockActionPiston(
IMyPistonBase piston,
float position,
float velocity = 2f
)
{
_piston = piston;
_position = position;
_velocity = velocity;
}
public IEnumerator<bool> Run()
{
_piston.MoveToPosition(_position, _velocity);
float lastValue = -1f;
while (lastValue != _piston.CurrentPosition)
{
lastValue = _piston.CurrentPosition;
yield return true;
}
}
}
}
}

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
namespace IngameScript
{
partial class Program
{
public class BlockActionRotor : IBlockAction
{
public bool Running { get; private set; } = false;
private IMyMotorStator _rotor;
private float _angle;
private float _velocity;
private MyRotationDirection _direction;
public BlockActionRotor(
IMyMotorStator rotor,
float angle,
float velocity = 5f,
MyRotationDirection direction = MyRotationDirection.AUTO
)
{
_rotor = rotor;
_angle = angle;
_velocity = velocity;
_direction = direction;
}
public IEnumerator<bool> Run()
{
_rotor.RotorLock = false;
_rotor.RotateToAngle(_direction, _angle, _velocity);
float _lastAngle = -1;
while (_rotor.Angle != _lastAngle)
{
_lastAngle = _rotor.Angle;
yield return true;
}
_rotor.RotorLock = true;
}
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
namespace IngameScript
{
partial class Program
{
public class BlockActionRotor : IBlockAction
{
private IMyMotorRotor _rotor;
private float _targetAngle;
private float _velocity = 5f;
private RotationDirection _direction = RotationDirection.AUTO;
public BlockActionRotor(
IMyMotorRotor rotor,
float targetAngle,
float velocity = 5f,
)
{
_rotor = rotor;
}
public IEnumerator<bool> Run()
{
// code to actually execute the action goes here
// any loops that wait for a condition to be true should
// `yield return true` inside the loop.
// If the action ends prematurely, use `yield break`
yield return true;
}
}
}
}

View File

@ -0,0 +1,17 @@
// An interface to represent a pre-configured action performed asynchronously
// on a block. Implements a Run() method that executes the action and provides an
// Enumerator to monitor when the action is complete.
using System.Collections.Generic;
namespace IngameScript
{
partial class Program
{
public interface IBlockAction
{
bool Running { get; }
IEnumerator<bool> Run();
}
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace IngameScript
{
partial class Program
{
public interface ISequenceable
{
bool Running { get; }
int Step { get; set; }
IEnumerator<bool> Run(bool deploy);
}
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace IngameScript
{
partial class Program
{
}
}

View File

@ -0,0 +1,132 @@
// A SequenceableBlock holds a reference to a compatible block type,
// and a list of available actions sorted by name.
// It provides a RunAction() method that allows the caller to
// run the configured actions.
using System;
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript
{
partial class Program
{
public interface ISequenceableAction
{
string Name { get; }
IEnumerator<bool> Run();
}
public class SequenceableBlock
{
private IConsole _console;
private IMyTerminalBlock _block;
private Dictionary<string, ISequenceableAction> _actions = new Dictionary<string, ISequenceableAction>();
public SequenceableBlock(IConsole console, IMyTerminalBlock block)
{
_console = console;
if (!(_block is IMyDoor || _block is IMyPistonBase || _block is IMyMotorRotor))
{
_console.Print("ERROR: Incompatible block '{_block.CustomName}' being sequenced.");
}
_block = block;
}
// We employ a pseudo-factory pattern to parse the settings
// into ISequenceableAction objects.
public void Parse(MyIni ini, string sectionName = "sequencer")
{
ini.TryParse(_block.CustomData, sectionName);
List<MyIniKey> keys = new List<MyIniKey>();
ini.GetKeys(sectionName, keys);
if (_block is IMyDoor)
{
parseDoor(ini, keys);
}
else if (_block is IMyPistonBase)
{
parsePiston(ini, keys);
}
else if (_block is IMyMotorRotor)
{
parseRotor(ini, keys);
}
}
// Alternatively, the user can parse manually and override anything they
// need to.
public void AddAction(ISequenceableAction action, string name)
{
_actions.Add(action.Name, action);
}
private void parseDoor(MyIni ini, List<MyIniKey> keys)
{
foreach (MyIniKey key in keys)
{
string[] values = ini.Get(key).ToString().Split(',');
string actionType = values[1];
bool lockDoor = true;
if (values.Length >= 3) lockDoor = values[2] == "true" ? true : false;
SequenceableActionDoor action = new SequenceableActionDoor(
_console,
key.Name,
_block as IMyDoor,
actionType,
lockDoor
);
_actions.Add(key.Name, action);
}
}
private void parsePiston(MyIni ini, List<MyIniKey> keys)
{
foreach (MyIniKey key in keys)
{
string[] values = ini.Get(key).ToString().Split(',');
float angle = Single.Parse(values[1]);
float velocity = 5f;
if (values.Length >= 3) velocity = Single.Parse(values[2]);
MyRotationDirection dir = MyRotationDirection.AUTO;
if (values.Length >= 4)
{
switch (values[3])
{
case "cw":
dir = MyRotationDirection.CW;
break;
case "ccw":
dir = MyRotationDirection.CCW;
break;
}
}
SequenceableActionPiston action = new SequenceableActionPiston(
_console,
key.Name,
_block as IMyPistonBase,
);
_actions.Add(key.Name, action);
}
}
private void parseRotor(MyIni ini, List<MyIniKey> keys)
{
foreach (MyIniKey key in keys)
{
string[] values = ini.Get(key).ToString().Split(',');
SequenceableActionRotor action = new SequenceableActionRotor();
_actions.Add(key.Name, action);
}
}
}
}
}

View File

@ -1,74 +0,0 @@
using Sandbox.ModAPI.Ingame;
using System.Collections.Generic;
using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript
{
partial class Program
{
public class SequenceableDoor : ISequenceable
{
public bool Running { get; private set; } = false;
public int Step { get; set; }
public bool DeployOpen { get; set; }
public bool LockOpen { get; set; }
public bool LockClosed { get; set; }
private IMyDoor _door;
public SequenceableDoor(
IConsoleProgram _program,
IMyDoor door,
string sectionName)
{
_door = door;
MyIni ini = _program.Ini;
ini.TryParse(door.CustomData);
DeployOpen = ini.Get(sectionName, "deployOpen").ToBoolean(true);
LockOpen = ini.Get(sectionName, "lockOpen").ToBoolean(true);
LockClosed = ini.Get(sectionName, "lockClosed").ToBoolean(true);
Step = ini.Get(sectionName, "step").ToInt32(0);
}
public IEnumerator<bool> Run(bool deploy)
{
if (Running) yield break;
Running = true;
if (deploy && DeployOpen || !deploy && !DeployOpen)
{
foreach (bool tick in _openDoor()) yield return true;
}
else
{
foreach (bool tick in _closeDoor()) yield return true;
}
Running = false;
}
public IEnumerable<bool> _openDoor()
{
_door.Enabled = true;
_door.OpenDoor();
while (_door.Status != DoorStatus.Open) yield return true;
if (LockOpen)
{
_door.Enabled = false;
}
}
public IEnumerable<bool> _closeDoor()
{
_door.Enabled = true;
_door.CloseDoor();
while (_door.Status != DoorStatus.Closed) yield return true;
if (LockClosed)
{
_door.Enabled = false;
}
}
}
}
}

View File

@ -1,38 +0,0 @@
// I hate Factories, but when the shoe fits...
using Sandbox.ModAPI.Ingame;
using SpaceEngineers.Game.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript
{
partial class Program
{
public class SequenceableFactory
{
public static ISequenceable MakeSequenceable(
IConsoleProgram program,
IMyTerminalBlock block,
string sectionName = "sequence")
{
if (block is IMyMotorStator)
{
return new SequenceableRotor(program, block as IMyMotorStator, sectionName);
}
if (block is IMyPistonBase)
{
return new SequenceablePiston(program, block as IMyPistonBase, sectionName);
}
if (block is IMyShipMergeBlock)
{
// return new SequenceableMergeBlock(block as IMyShipMergeBlock, step);
}
if (block is IMyDoor)
{
return new SequenceableDoor(program, block as IMyDoor, sectionName);
}
return null;
}
}
}
}

View File

@ -1,55 +0,0 @@
using Sandbox.ModAPI.Ingame;
using System;
using System.Collections.Generic;
using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript
{
partial class Program
{
public class SequenceablePiston : ISequenceable
{
public bool Running { get; private set; } = false;
public int Step { get; set; }
private IConsoleProgram _program;
private IMyPistonBase _piston;
private float _deployPosition;
private float _stowPosition;
private float _velocity;
public SequenceablePiston(IConsoleProgram program, IMyPistonBase piston, string sectionName)
{
_program = program;
_piston = piston;
MyIni ini = _program.Ini;
ini.TryParse(piston.CustomData);
_deployPosition = ini.Get(sectionName, "deployPosition").ToSingle(10F);
_stowPosition = ini.Get(sectionName, "stowPosition").ToSingle(0F);
_velocity = ini.Get(sectionName, "velocity").ToSingle(5F);
Step = ini.Get(sectionName, "step").ToInt32(0);
}
public IEnumerator<bool> Run(bool deploy)
{
if (Running) yield break;
Running = true;
float targetValue = _stowPosition;
float lastValue = -1;
if (deploy) targetValue = _deployPosition;
_piston.MoveToPosition(targetValue, _velocity);
while (lastValue != _piston.CurrentPosition)
{
lastValue = _piston.CurrentPosition;
yield return true;
}
Running = false;
}
}
}
}

View File

@ -1,81 +0,0 @@
using Sandbox.ModAPI.Ingame;
using System;
using System.Collections.Generic;
using VRage.Game.ModAPI.Ingame.Utilities;
namespace IngameScript
{
partial class Program
{
public class SequenceableRotor : ISequenceable
{
public bool Running { get; private set; } = false;
public int Step { get; set; }
private IConsoleProgram _program;
private float _velocity;
private float _deployAngle;
private float _stowAngle;
private IMyMotorStator _rotor;
private MyRotationDirection _deployDirection;
private MyRotationDirection _stowDirection;
public SequenceableRotor(
IConsoleProgram program,
IMyMotorStator rotor,
string sectionName)
{
_program = program;
_rotor = rotor;
MyIni ini = _program.Ini;
ini.TryParse(rotor.CustomData);
_deployAngle = ini.Get(sectionName, "deployAngle").ToSingle(90F);
_stowAngle = ini.Get(sectionName, "stowAngle").ToSingle(0F);
_velocity = ini.Get(sectionName, "velocity").ToSingle(5F);
switch (ini.Get(sectionName, "deployDirection").ToString("auto"))
{
case "auto":
_deployDirection = MyRotationDirection.AUTO;
_stowDirection = MyRotationDirection.AUTO;
break;
case "cw":
_deployDirection = MyRotationDirection.CW;
_stowDirection = MyRotationDirection.CCW;
break;
case "ccw":
_deployDirection = MyRotationDirection.CCW;
_stowDirection = MyRotationDirection.CW;
break;
}
Step = ini.Get(sectionName, "step").ToInt32(0);
}
public IEnumerator<bool> Run(bool deploy = true)
{
Running = true;
float degAngle = deploy ? _deployAngle : _stowAngle;
MyRotationDirection dir = deploy ? _deployDirection : _stowDirection;
_rotor.RotorLock = false;
_rotor.RotateToAngle(dir, degAngle, _velocity);
float _lastAngle = -1;
while (_rotor.Angle != _lastAngle)
{
_lastAngle = _rotor.Angle;
yield return true;
}
_rotor.RotorLock = true;
Running = false;
}
private float degToRad(float degrees)
{
return degrees * ((float)Math.PI / 180F);
}
}
}
}

View File

@ -1,74 +0,0 @@
// Represents a single set of blocks to run in order, waiting for each group's completion before
// moving on to the next one.
using Sandbox.ModAPI.Ingame;
using System.Collections.Generic;
using System.Linq;
namespace IngameScript
{
partial class Program
{
public class Sequencer
{
public bool Running { get; private set; }
public string Name { get; private set; }
private IConsole _console;
private SortedDictionary<int, List<ISequenceable>> _sequence = new SortedDictionary<int, List<ISequenceable>>();
public Sequencer(IConsoleProgram program, string name)
{
Name = name;
_console = new PrefixedConsole(program.Console, Name);
}
public void AddBlock(ISequenceable block)
{
if (!_sequence.ContainsKey(block.Step)) _sequence[block.Step] = new List<ISequenceable>();
_sequence[block.Step].Add(block);
}
// To activate the Sequencer, call this once, then call `MoveNext()` once per tick
// on the returned object until it returns false.
// (then be sure to call `Dispose() on that object`)
public IEnumerator<bool> RunSequence(bool deploy = true)
{
if (Running)
{
_console.Print("Already running, ignoring invocation.");
yield break;
}
// This is a bit convoluted, but we're extracting the iterator for use in the foreach directly.
// This allows us to iterate in reverse when reversing/"closing" the sequence.
IEnumerable<KeyValuePair<int, List<ISequenceable>>> steps = deploy ? _sequence : _sequence.Reverse();
foreach (KeyValuePair<int, List<ISequenceable>> kvp in steps)
{
List<ISequenceable> blocks = kvp.Value;
List<IEnumerator<bool>> subJobs = new List<IEnumerator<bool>>();
Running = true;
foreach (ISequenceable block in blocks)
{
// TODO: add some sort of handling for block.Running == true, maybe?
// It *should* be an impossible case, but...
subJobs.Add(block.Run(deploy));
}
while (Running)
{
Running = false;
foreach (IEnumerator<bool> subJob in subJobs)
{
if (subJob.MoveNext()) Running = true;
}
yield return true;
}
foreach (IEnumerator<bool> subJob in subJobs) subJob.Dispose(); // clean up after ourselves
}
}
}
}
}

View File

@ -6,6 +6,11 @@
<SharedGUID>8a3cdcc5-4b55-4d87-a415-698a0e1ff06f</SharedGUID>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)/**/*.cs" Visible=" '$(ShowCommonFiles)' == 'true' " />
<Compile Include="$(MSBuildThisFileDirectory)ActionGroup.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ActionSequence.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BlockActionDoor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BlockActionPiston.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BlockActionRotor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)IBlockAction.cs" />
</ItemGroup>
</Project>