Skip to content

Instantly share code, notes, and snippets.

@awstanley
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save awstanley/97da2725d695e7ee2eae to your computer and use it in GitHub Desktop.
Save awstanley/97da2725d695e7ee2eae to your computer and use it in GitHub Desktop.
Space Engineers related code. Stripped back example of the lag causing code. (Updated to include DateTime code, typed manually into it from newer code; it should work, and I tested the error-free nature of it briefly in creative on a random block).
//---------------------------------------------------------------------
// Configuration variables. Define this once, use it everywhere.
// (Save yourself the headache of rewriting it over and over again...)
//
// Two key points here:
// (1) All points should be out of standard usage (e.g. ☼ § « »);
// (2) Anything which MAY need to be typed (ever), should be available
// easily via ASCII or CharMap (for user access). Anything else
// (list delimiting) should be done sanely but copy+paste should
// be more than enough.
//---------------------------------------------------------------------
public class Config
{
// ☼ 15 for data
public char Marker_Data;
// « 174 for between entries
// Default List: <entry>«<entry>«<entry>«
// Note: trailing « is maintained to keep it simple for writing;
// and reading will skip anything with 0 length
// (This also allows for safe/simple manual deletes.)
public char Storage_Split;
// » 175 for k/v split (=)
// Default Key/Value syntax: <key>◄<value>
public char Storage_KV;
// The things below don't need to be typed, pull the info out of the font
// data SpaceEngineers\Content\Fonts\blue\FontData.xml
// Just make sure they're not blank or missing glyphs :)
// ® for requests
public char Marker_Request;
// ¬ for our 'core' split in keys
// Default syntax of keys: <type>¬<specifics>
public char Key_Split;
// ¶ 20 for custom name splits
public char CustomName_Split;
// § for interface split
public char Interface_Split;
// Handlers we use (register these in Register())
// Handler proto: bool Handler(DataSet, DataSet, Key, Value)
public Dictionary<String, Func<Config, DataSet, DataSet,
String, String, bool>> Handlers;
public Config()
{
Marker_Data = '☼';
Storage_Split = '«';
Storage_KV = '»';
Marker_Request = '®';
Key_Split = '¬';
CustomName_Split = '¶';
Interface_Split = '§';
Handlers = new Dictionary<String, Func<Config, DataSet, DataSet,
String, String, bool>>();
}
}
//--------------------------------------------------------------
// Actions
//--------------------------------------------------------------
// Switch lock
ITerminalAction ActionConnector_SwitchLock;
//--------------------------------------------------------------
// DataSet for handling information.
//--------------------------------------------------------------
// A data set contains requests and data. Unfortunately we can't
// store this in an array (a List<> or Dictionary<>, for example).
public class DataSet
{
private Config Cfg;
// Should attempt to save on quit (only for public data)
private bool AutoSave;
// Block name (public so we can do bad things)
public string Name;
// Load from Storage string and not CustomName
private bool StorageNotName;
// Actual block
public IMyTerminalBlock Block;
// Request storage
public Dictionary<String, String> Requests;
// Data storage
public Dictionary<String, String> Data;
// ID is required for directing things.
public String GetID()
{
if(!IsValid())
{
return "";
}
String Out = Name + Cfg.Interface_Split;
if(StorageNotName)
{
// A interface is 'Private'
Out += "A";
}
else
{
// B interface is 'Public'
Out += "B";
}
return Out;
}
// Validity check for processing
public bool IsValid()
{
if(StorageNotName)
{
return true;
}
if(Block != null)
{
return true;
}
return false;
}
// Load a request (from string to storage)
public void AddRequest(String Req)
{
// We trust the string to be a request (as we control the code),
// so we split it using the KV key
String[] Frags = Req.Split(Cfg.Storage_KV);
// Store
Requests[Frags[0]] = Frags[1];
}
// Load datum (from string to storage)
public void AddData(String Dat)
{
// We trust the string to be datum (as we control the code),
// so we split it using the KV key
String[] Frags = Dat.Split(Cfg.Storage_KV);
// Store
Data[Frags[0]] = Frags[1];
}
// Load array of information
private void Load(String Data)
{
String[] Entries = Data.Split(Cfg.Storage_Split);
for (int i = 0; i < Entries.Length; i++)
{
// Get the type, using the prefix
if (Entries[i].Length > 0)
{
if (Entries[i][0] == Cfg.Marker_Request)
{
AddRequest(Entries[i].Substring(1));
}
else if (Entries[i][0] == Cfg.Marker_Data)
{
AddData(Entries[i].Substring(1));
}
}
}
}
// Save when we're done, dumping the string out at the end
public String SaveToString()
{
StringBuilder SB = new StringBuilder();
// Circumvent foreach bugs by using the list
var DataKeys = new List<string>(Data.Keys);
for (int i = 0; i < DataKeys.Count; i++)
{
// Data
SB.Append(Cfg.Marker_Data + DataKeys[i] +
Data[DataKeys[i]] + Cfg.Storage_Split);
}
// Circumvent foreach bugs by using the list
var RequestKeys = new List<string>(Requests.Keys);
for (int i = 0; i < RequestKeys.Count; i++)
{
// Requests
SB.Append(Cfg.Marker_Request + RequestKeys[i] +
Requests[RequestKeys[i]] + Cfg.Storage_Split);
}
// Return the string form
return SB.ToString();
}
public void Save()
{
if (AutoSave && !StorageNotName)
{
String[] Frags = Block.CustomName.Split(Cfg.CustomName_Split);
if (Frags.Length >= 3)
{
StringBuilder SB = new StringBuilder();
SB.Append(Frags[0]);
SB.Append(Cfg.CustomName_Split);
SB.Append(Frags[1]);
SB.Append(Cfg.CustomName_Split);
SB.Append(SaveToString());
for (int i = 3; i < Frags.Length; i++)
{
SB.Append(Cfg.CustomName_Split);
SB.Append(Frags[i]);
}
// Save to our shared memory
Block.SetCustomName(SB);
}
}
}
// Process the internal data, and only that data.
public void Process()
{
// Do not process if invalid, as we can't save.
if(!IsValid())
{
return;
}
// For given requests we're interested in running them only once.
// Running parallel updates (e.g. "Lock" + "Lock") would fail here;
// the design is simple enough. The workaround is simple.
// Essentially you want to process the requests using known keys,
// but the trick here is to split the keys so you can use them
// in multiple places with the same handler.
// We want to store an array of things which are processed
// so that we can remove them without messing up the in-loop work.
List<String> ToPop = new List<String>();
// Declare once
int i = 0;
// Circumvent foreach bugs by using the list
var RequestKeys = new List<string>(Requests.Keys);
for (i = 0; i < RequestKeys.Count; i++)
{
// Get the key fragments
String[] Frags = RequestKeys[i].Split(Cfg.Key_Split);
// We're using the syntax (with default split being ¬):
// <type><Key_Split><blockid>
// If we have a handler, use it.
if(Cfg.Handlers.ContainsKey(Frags[0]))
{
// Remove from the request queue if we executed it
if(Cfg.Handlers[Frags[0]](
Cfg, this, null,
RequestKeys[i],
Requests[RequestKeys[i]]
))
{
ToPop.Add(RequestKeys[i]);
}
}
}
// Remove anything in the ToPop array
for(i = 0; i < ToPop.Count; i++)
{
Requests.Remove(ToPop[i]);
}
}
// Process the requests between blocks, removing requests
// which have been processed. See Process()
public void Process(ref DataSet that)
{
List<String> ToPop = new List<String>();
// Initialise Requests
var REQ = new Dictionary<String, String>();
List<String> RequestKeys;
// Declare once
int i = 0;
{
RequestKeys = new List<String>(Requests.Keys);
for (i = 0; i < RequestKeys.Count; i++)
{
REQ[RequestKeys[i]] = Requests[RequestKeys[i]];
}
RequestKeys = new List<String>(that.Requests.Keys);
for (i = 0; i < RequestKeys.Count; i++)
{
REQ[RequestKeys[i]] = that.Requests[RequestKeys[i]];
}
}
// Circumvent foreach bugs by using the list
RequestKeys = new List<string>(REQ.Keys);
for (i = 0; i < RequestKeys.Count; i++)
{
// Get the key fragments
String[] Frags = RequestKeys[i].Split(Cfg.Key_Split);
// We're using the syntax (with default split being ¬):
// <type><Key_Split><blockid>
//
// "Execute only if after" related code MUST be performed
// in the handler, but can be done using a common function.
if (Cfg.Handlers.ContainsKey(Frags[0]))
{
// Remove from the request queue if we executed it
if (Cfg.Handlers[Frags[0]](
Cfg, this, null,
RequestKeys[i],
REQ[RequestKeys[i]]
))
{
ToPop.Add(RequestKeys[i]);
}
}
}
// Remove anything in the ToPop array
for (i = 0; i < ToPop.Count; i++)
{
Requests.Remove(ToPop[i]);
}
// Same for the other block's requests
for (i = 0; i < ToPop.Count; i++)
{
that.Requests.Remove(ToPop[i]);
}
}
// Default constructor (loads storage)
public DataSet(Config Configuration, String DataString)
{
Cfg = Configuration;
AutoSave = false;
// We're using storage here
StorageNotName = true;
// Initialise Requests
Requests = new Dictionary<String, String>();
// Initialise Data
Data = new Dictionary<String, String>();
// Load from storage
if (DataString != "")
{
Load(DataString);
}
}
// Default constructor (loads storage)
public DataSet(Config Configuration, IMyTerminalBlock InBlock, bool SaveOnQuit)
{
Cfg = Configuration;
AutoSave = SaveOnQuit;
// Set the block
Block = InBlock;
// We're using storage here
StorageNotName = false;
// Initialise Requests
Requests = new Dictionary<String, String>();
// Initialise Data
Data = new Dictionary<String, String>();
// Safely load from CustomName (our shared memory)
if (Block != null)
{
String[] Frags = Block.CustomName.Split(Cfg.CustomName_Split);
if (Frags.Length >= 3)
{
if (Frags[2].Length != 0)
{
Load(Frags[2]);
Name = Frags[1];
}
}
}
}
// Triggers on deletion
protected void Finalize()
{
Save();
}
}
//--------------------------------------------------------------
// Request Manipulation/Addition
//--------------------------------------------------------------
// Data -- block in which to store it;
// Cfg -- the configuration (determining split code);
// BlockName -- this block name needs to be the named block
// otherwise the lock won't execute, for example
// "DockCode|SSI0", when loading this, will execute
// the lock, but no other block will;
// Tag -- Connector name, in this case;
// Lock -- true/false (converted to/from string)
void AddLockRequest(ref DataSet Data,
Config Cfg, String BlockName,
String Tag, bool Lock)
{
String Key =
// Key
"LockConnector" +
// Key split
Cfg.Key_Split +
//
BlockName;
// Build the value:
String Value =
// Tag
Tag +
// Interface split
Cfg.Interface_Split +
// Use 0 to indicate we don't care
Lock.ToString();
Data.AddRequest(Key + Cfg.Storage_KV + Value);
}
// See AddLockRequest, with the addition:
// OnlyAfter is the systemtime after which it may be executed
// TODO: When we get simulation time access, port it to simulation time.
void AddLockRequest_Timed(ref DataSet Data,
Config Cfg, String BlockName,
String Tag, bool Lock, System.DateTime OnlyAfter)
{
String Key =
// Key
"LockConnector" +
// Key split
Cfg.Key_Split +
// Block which may execute this request
BlockName +
// Key split
Cfg.Key_Split +
// Execute only after
OnlyAfter.ToBinary().ToString();
// Build the value:
String Value =
// Tag
Tag +
// Interface split
Cfg.Interface_Split +
// Use 0 to indicate we don't care
Lock.ToString();
}
//--------------------------------------------------------------
// Block Magic
//--------------------------------------------------------------
bool Connector_SetLock(IMyShipConnector Block, bool SetLock)
{
if (Block.IsLocked != SetLock)
{
ActionConnector_SwitchLock.Apply(Block);
}
return Block.IsLocked;
}
//--------------------------------------------------------------
// Actions
//--------------------------------------------------------------
// Internal function for switching/locking connectors
public bool _LockConnector_Unguarded(Config Cfg, DataSet DSA, DataSet DSB, String[] Frags, String Value)
{
// We'll reuse this, as the interface is
// an internal value distinct from Value
String[] VFrags = Value.Split(Cfg.Interface_Split);
// Lock state
bool Lock = false;
String ConnectorName = "";
// Syntax: <Name>§<Lock>
if (VFrags.Length > 2)
{
ConnectorName = VFrags[0];
Lock = Convert.ToBoolean(VFrags[1]);
}
if (ConnectorName != "")
{
// Get the connector as we're stuck on a single grid;
// we don't need DSA/DSB.
IMyShipConnector Connector = (IMyShipConnector)
GetTaggedBlockData<IMyShipConnector>(Cfg, Frags[1]);
// Lock it if the connector was found.
if (Connector != null)
{
// If it's fine, let it be (and remove it).
if (Lock == Connector.IsLocked)
{
return true;
}
// We'll use an external function to hnadle this
Connector_SetLock(Connector, Lock);
// Assume nothing with toggles.
return false;
}
}
return false;
}
public bool Handler_LockConnector(
Config Cfg,
DataSet DSA, DataSet DSB,
String Key, String Value)
{
String[] Frags = Key.Split(Cfg.Key_Split);
switch(Frags.Length)
{
// Key should be "LockConnector" + KeySplit + BlockName
case 2:
{
return _LockConnector_Unguarded(Cfg, DSA, DSB, Frags, Value);
} break;
// We execute case 3 as a guarded version of case 2 as Frags[2] is the DateTime we want.
case 3:
if (System.DateTime.FromBinary(Convert.ToInt64(Frags[2])) <= System.DateTime.Now)
{
return _LockConnector_Unguarded(Cfg, DSA, DSB, Frags, Value);
} break;
// All other states fail
default:
break;
}
// Unhandled
return false;
}
//--------------------------------------------------------------
// Helpers
//--------------------------------------------------------------
// Get data from the data segment of a block's CustomName
// Expected syntax: <friendly>¶<tag>¶<data>
public IMyTerminalBlock GetTaggedBlockData<TInterface>(Config Cfg, String Tag)
{
var Blocks = new List<IMyTerminalBlock>();
GridTerminalSystem.GetBlocksOfType<TInterface>(Blocks);
for(int i = 0; i < Blocks.Count; i++)
{
String[] Frags = Blocks[i].CustomName.Split(Cfg.CustomName_Split);
if(Frags.Length >= 3)
{
if(Frags[1] == Tag)
{
return Blocks[i];
}
}
}
return null;
}
// Use the above function to pull a DataSet.
public DataSet LoadDataSetFromBlockName<TInterface>(Config Cfg, String Name, bool SaveOnQuit)
{
IMyTerminalBlock Block = GetTaggedBlockData<TInterface>(Cfg, Name);
if (Block != null)
{
return new DataSet(Cfg, Block, SaveOnQuit);
}
return null;
}
//--------------------------------------------------------------
// Core execution
//--------------------------------------------------------------
// Load actions from inside the game into the global variables.
// Note: these actions should only be used in handlers, not inside DataSets,
// and should be expected to fail inside data sets due to their non-static
// nature (we can't use statics, hence not being static).
public void Init()
{
var ConnectorBlocks = new List<IMyTerminalBlock>();
GridTerminalSystem.GetBlocksOfType<IMyShipConnector>(ConnectorBlocks);
if(ConnectorBlocks.Count > 0)
{
ActionConnector_SwitchLock = ConnectorBlocks[0].GetActionWithName("Switch lock");
}
}
// This function handles the registration of callbacks for use in Process()
public void Register(ref Config Cfg)
{
// e.g. "LockConnector¬<BlockID>§B»<RequestInformation>"
Cfg.Handlers.Add(
"LockConnector",
Handler_LockConnector
);
// Add other handlers (functions) here.
}
void Main()
{
// No static variables... kill me
Config Cfg = new Config();
// Get the actions
Init();
// Register
Register(ref Cfg);
// We need this block to perform request/response properly. However,
// if we want to leverage this block for private/public information
// we might as well create two data sets:
// ThisPublic (CustomName) and ThisPrivate (Storage)
// Get public first (we want to save on quit)
DataSet ThisPublic = LoadDataSetFromBlockName<IMyProgrammableBlock>(Cfg, "CodeBlock|0", true);
// If the block wasn't found we're screwed and nothing following will
// work as intended, so bail.
if (ThisPublic == null)
{
return;
}
// This uses Storage instead of CustomName.
// Note: Storage is non-static, so we have to pass it in explicitly here.
DataSet ThisPrivate = new DataSet(Cfg, Storage);
// If we get to ThisPrivate then we have access to ThisPublic,
// lift the block and drop it into place (this is why it's public).
ThisPrivate.Block = ThisPublic.Block;
// We need a name, so steal that too:
ThisPrivate.Name = ThisPublic.Name;
// We'll use an external storage block (an interior light) in this example
// to demonstrate the power of requests.
DataSet Storage0 = LoadDataSetFromBlockName<IMyLightingBlock>(Cfg, "Storage|0", true);
// We don't need to check for ThisPublic or ThisPrivate being null,
// since we'd have bailed if ThisPublic failed.
//
// If ThisPrivate failed then it's a non-issue provided it saves.
// Start by processing This block (both interfaces)
ThisPublic.Process();
ThisPrivate.Process();
// Sync Public/Private (as other blocks write to public)
ThisPrivate.Process(ref ThisPublic);
// Now, in this example, we process Storage0 to remove standing
// requests (before we add our own)
if (Storage0 != null)
{
// Process the internal state to sanity (where possible)
Storage0.Process();
// Process Public
ThisPublic.Process(ref Storage0);
// Process Private
ThisPublic.Process(ref Storage0);
// Process Public/Private
ThisPublic.Process(ref ThisPrivate);
}
// Perform whatever (other) actions you want to add more requests,
// or whatever else here, or between the above.
// On close our processed blocks will be automatically deleted;
// the garbage collector triggers the deconstructor (in our
// case ~Finalize() is executed), and if it can be saved
// the data will be saved in its updated form (based on what
// we need and/or want).
// The exception here is Storage is non-static, so we can't
// actually save data to it normally. We have to explicitly
// call this :(
Storage = ThisPrivate.SaveToString();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment