diff --git a/AirMonitor/AirMonitor.csproj b/AirMonitor/AirMonitor.csproj
index 4126f8c..9e4a90c 100644
--- a/AirMonitor/AirMonitor.csproj
+++ b/AirMonitor/AirMonitor.csproj
@@ -25,4 +25,5 @@
+
\ No newline at end of file
diff --git a/AirMonitor/Program.cs b/AirMonitor/Program.cs
index 77a0904..bc15915 100644
--- a/AirMonitor/Program.cs
+++ b/AirMonitor/Program.cs
@@ -13,6 +13,7 @@ namespace IngameScript
public MyIni Ini { get; } = new MyIni();
public IConsole Console { get; private set; }
+ private FailoverManager _failover;
private List> _jobs = new List>();
private Dictionary _zones = new Dictionary();
@@ -27,6 +28,7 @@ namespace IngameScript
public Program()
{
Console = new MainConsole(this, "Air Pressure Monitor");
+ _failover = new FailoverManager(this, Console, Ini);
List surfaces = new List();
@@ -116,6 +118,7 @@ namespace IngameScript
public void Main(string argument, UpdateType updateSource)
{
Console.UpdateTickCount();
+ if (_failover.ActiveCheck() == FailoverState.Standby) return;
// Light indicators should be set/unset independent of the triggered states and actions.
foreach (IMyLightingBlock light in _lights) light.Color = Color.White;
diff --git a/Mixins/Console/Console.cs b/Mixins/Console/Console.cs
index 7dcd8ea..a18df42 100644
--- a/Mixins/Console/Console.cs
+++ b/Mixins/Console/Console.cs
@@ -135,5 +135,29 @@ namespace IngameScript
public void PrintLower(string text) { }
public void UpdateTickCount() { }
}
+
+ // A replacement for the main console if you don't want the display screens involved.
+ // Good if you're using the Programmable Block's screens for your own purposes, but still
+ // need PrefixedConsole support.
+ //
+ // Content will just be printed via Echo, and PrintLower and UpdateTickCount will simply print
+ // an error.
+ public class EchoConsole : IConsole
+ {
+ private Program _program;
+
+ public EchoConsole(Program program)
+ {
+ _program = program;
+ }
+
+ public void Print(string text)
+ {
+ _program.Echo(text);
+ }
+
+ public void PrintLower(string text) { }
+ public void UpdateTickCount() { }
+ }
}
}
\ No newline at end of file
diff --git a/Mixins/FailoverManager/FailoverManager.cs b/Mixins/FailoverManager/FailoverManager.cs
new file mode 100644
index 0000000..43e5ebc
--- /dev/null
+++ b/Mixins/FailoverManager/FailoverManager.cs
@@ -0,0 +1,101 @@
+using Sandbox.ModAPI.Ingame;
+using System.Collections.Generic;
+
+namespace IngameScript
+{
+ partial class Program
+ {
+ [Flags]
+ public enum FailoverState
+ {
+ Standby = 0x1,
+ Failover = 0x2,
+ Active = 0x4,
+ }
+
+ public class FailoverManager
+ {
+ private Program _program;
+ private IConsole _console;
+
+ private string _id;
+ private int _rank;
+ private bool _active = false;
+ private SortedDictionary _nodes = new SortedDictionary();
+
+ public FailoverManager(Program program, IConsole console, MyIni ini)
+ {
+ _program = program;
+ _console = new PrefixedConsole(console, "FailoverManager");
+ _ini = ini;
+
+ // Read our config
+ bool configExists = _ini.TryParse(_program.Me.CustomData);
+ if (!configExists)
+ {
+ _console.Print("No failover config, assuming we are single instance.");
+ _active = true;
+ return;
+ }
+
+ _id = _ini.Get("failover", "id").ToString();
+ _rank = _ini.Get("failover", "rank").ToInt32(-1);
+
+ if (_id == "" || _rank == -1)
+ {
+ _console.Print("Failover config invalid. Assuming we are single instance.");
+ _active = true;
+ return;
+ }
+
+ List allPBs = new List();
+ _program.GridTerminalSystem.GetBlocksOfType(allPBs, blockFilter);
+
+ foreach (IMyProgrammableBlock node in allPBs)
+ {
+ _ini.TryParse(node.CustomData);
+
+ string foreignId = _ini.Get("failover", "id");
+ if (foreignId != _id) continue;
+
+ int foreignRank = _ini.Get("failover", "rank");
+ if (foreignRank == -1) continue;
+
+ _nodes[foreignRank] = node;
+ }
+ }
+
+ // returns true if we should become the active node
+ public FailoverState ActiveCheck()
+ {
+ // Once we're the active node, we will stay active for the life of the script.
+ // TODO: test the assumption that the script restarts from scratch on disable/enable...
+ // if not we may need additional logic.
+ if (_active) return FailoverState.Active;
+
+ foreach (KeyValuePair kvp in _nodes)
+ {
+ // If we have a higher priority than the nodes we've checked so far,
+ // we must be the active node.
+ if (_rank < kvp.Key)
+ {
+ _active = true;
+ return FailoverState.Failover | FailoverState.Active;
+ }
+
+ // We've found an active node with higher priority than us.
+ if (!kvp.Value.Closed && kvp.Value.Enabled) return FailoverState.Standby;
+ }
+
+ // We're the last node standing. Let's hope we don't go down.
+ _active = true;
+ return FailoverState.Failover | FailoverState.Active;
+ }
+
+ private bool blockFilter(IMyProgrammableBlock block)
+ {
+ return block.IsSameConstructAs(_program.Me) && MyIni.HasSection(block.CustomData, "failover");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Mixins/FailoverManager/FailoverManager.projitems b/Mixins/FailoverManager/FailoverManager.projitems
new file mode 100644
index 0000000..2083cbc
--- /dev/null
+++ b/Mixins/FailoverManager/FailoverManager.projitems
@@ -0,0 +1,15 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 8a3cdcc5-4b55-4d87-a415-698a0e1ff06f
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mixins/FailoverManager/FailoverManager.shproj b/Mixins/FailoverManager/FailoverManager.shproj
new file mode 100644
index 0000000..b544a9d
--- /dev/null
+++ b/Mixins/FailoverManager/FailoverManager.shproj
@@ -0,0 +1,19 @@
+
+
+
+ 8a3cdcc5-4b55-4d87-a415-698a0e1ff06f
+ 14.0
+
+
+
+
+
+
+ bin\Debug\
+
+
+ bin\Release\
+
+
+
+
diff --git a/Mixins/FailoverManager/readme.md b/Mixins/FailoverManager/readme.md
new file mode 100644
index 0000000..02dcc0a
--- /dev/null
+++ b/Mixins/FailoverManager/readme.md
@@ -0,0 +1,62 @@
+# Failover Manager for SE scripts
+
+This mixin allows multiple Programmable Blocks to coordinate with each other so only one copy of the script is running at a time; all but one block act as backup nodes that will be automatically activated (in priority order) in the event the active node goes offline.
+
+## Prerequisites
+
+* Everything assumes you're using [MDK2](TODO). Using this without MDK is possible, but is outside the scope of this documentation. (You probably just need to copy everything inside the `partial class Program` block for each mixin you're using into your own script, but I can't guarantee support for problems you encounter)
+* This requires my Console mixin as well. If you don't want to use that mixin's full functionality, you can use an EchoConsole (example below).
+* If you aren't using MyIni to configure your script's blocks, you can also just pass `new MyIni()` and avoid instantiating an entire property
+
+## Usage
+
+This code is designed to be as drop-in as possible once you meet the prerequisites above. Usage should look something like this:
+
+```
+ public partial class Program : MyGridProgram
+ public MyIni Ini { get; } = new MyIni();
+ public IConsole Console { get; private set; }
+
+ private FailoverManager _failover;
+
+ public Program() {
+ Console = new MainConsole(this, "Script Name");
+ _failover = new FailoverManager(this, Console, Ini);
+
+ // alternate instantiation with Stub Console and fresh MyIni instance.
+ // If you do it this way, you don't need the `Ini` or `Console` properties above.
+ // _failover = new FailoverManager(this, new EchoConsole(this), new MyIni());
+
+ // The script *must* start out with periodic updates, even if it is
+ // a script that's only triggered conditionally (though in that case),
+ // see caveats.
+ Runtime.UpdateFrequency = UpdateFrequency.Update100;
+
+ // ... script initialization happens here
+ }
+
+ public void Main(string argument, UpdateType updateSource) {
+ // This basically instructs the script to "go back to sleep"
+ // if it isn't the active node.
+ if (_failover.ActiveCheck == FailoverState.Standby) return;
+
+ // If you need more complicated "warm-up" logic when your script
+ // becomes active, (for example, changing the update frequency) you can use this.
+ // This statement will only be true once, when the script first takes over control.
+ //
+ // This is optional and depends on your use case.
+ if (_failover.ActiveCheck.HasFlag(FailoverState.Failover)) {
+ // Code that should run once when this script becomes the active node goes here
+ }
+
+ // ... your script logic goes here
+ }
+```
+
+Scripts that are using failover also need configuration in their Programmable Blocks' Custom Data. That should look something like this:
+
+```
+[failover]
+id=scriptName # This must match for all running copies of the script.
+rank=0 # The lowest ranked copy of the script will be the primary node, and they will failover in ascending rank order
+```
\ No newline at end of file
diff --git a/SpaceEngineers.sln b/SpaceEngineers.sln
index e13715b..81f34ce 100644
--- a/SpaceEngineers.sln
+++ b/SpaceEngineers.sln
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DockLoader", "DockLoader\Do
EndProject
Project("{8A3CDCC5-4B55-4D87-A415-698A0E1FF06F}") = "ScaledGridDisplay", "Mixins\ScaledGridDisplay\ScaledGridDisplay.shproj", "{1C1031E3-B1FE-43AE-91F1-BA74D854B69D}"
EndProject
+Project("{8A3CDCC5-4B55-4D87-A415-698A0E1FF06F}") = "FailoverManager", "Mixins\FailoverManager\FailoverManager.shproj", "{1DB3AF33-E322-4820-8678-B1EDA714E5C3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU