Merge pull request #53 from LazyDuchess/vm

Initial SimAntics VM Implementation
This commit is contained in:
Nahuel Rocchetti 2023-08-24 14:07:27 -03:00 committed by GitHub
commit 757eb53777
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1640 additions and 1 deletions

View file

@ -1,4 +1,6 @@
using System;
using OpenTS2.Common;
using OpenTS2.Files.Formats.DBPF;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -8,6 +10,8 @@ namespace OpenTS2.Content.DBPF
{
public class ObjectDefinitionAsset : AbstractAsset
{
// TODO: Person and Template object types seem to use 0x80 as the SG instance id for some reason.
public SemiGlobalAsset SemiGlobal => ContentProvider.Get().GetAsset<SemiGlobalAsset>(new ResourceKey(1, GlobalTGI.GroupID, TypeIDs.SEMIGLOBAL));
public string FileName;
public enum FieldNames

View file

@ -5,6 +5,7 @@ using OpenTS2.Diagnostic;
using OpenTS2.Files;
using OpenTS2.Files.Formats.DBPF;
using OpenTS2.Rendering;
using OpenTS2.SimAntics.Primitives;
using UnityEngine;
namespace OpenTS2.Engine
@ -34,6 +35,7 @@ namespace OpenTS2.Engine
Filesystem.Initialize(new JSONPathProvider(), epManager);
CodecAttribute.Initialize();
CheatSystem.Initialize();
VMPrimitiveRegistry.Initialize();
//Initialize the game assembly, do all reflection things.
AssemblyHelper.InitializeLoadedAssemblies();
s_initialized = true;

View file

@ -49,6 +49,7 @@ namespace OpenTS2.Files.Formats.DBPF
public const uint DLL = 0x7582DEC6;
public const uint CTSS = 0x43545353;
public const uint UI = 0x0;
public const uint BHAV = 0x42484156;
public const uint SEMIGLOBAL = 0x7F8D70BF;
}
public static class GroupIDs
@ -60,5 +61,6 @@ namespace OpenTS2.Files.Formats.DBPF
public const uint DIR = 0xE86B1EEF;
public const uint Scenegraph = 0x1C0532FA;
public const uint Effects = 0xEA5118B1;
public const uint Global = 0x7FD46CD0;
}
}

8
Assets/Scripts/OpenTS2/SimAntics.meta generated Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e31a40caa8341a846bd8243595805e3d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTS2.Content;
namespace OpenTS2.SimAntics
{
/// <summary>
/// Script in the SimAntics language. Handles most of the simulation.
/// </summary>
public class BHAVAsset : AbstractAsset
{
public string FileName = "";
public int ArgumentCount = 0;
public int LocalCount = 0;
public List<Node> Nodes = new List<Node>();
public class Node
{
public const ushort ErrorReturnValue = 0xFFFC;
public const ushort TrueReturnValue = 0xFFFD;
public const ushort FalseReturnValue = 0xFFFE;
public ushort OpCode;
public ushort TrueTarget;
public ushort FalseTarget;
public byte[] Operands;
public byte Version;
/// <summary>
/// Retrieves the operand at index.
/// </summary>
/// <returns>Operand byte value, or 0 if the index is out of range.</returns>
public byte GetOperand(int index)
{
if (index < 0)
return 0;
if (index >= Operands.Length)
return 0;
return Operands[index];
}
/// <summary>
/// Retrieves an operand byte array starting at index and of the specified length.
/// </summary>
/// <returns>Operand bytes, bytes out of range return 0.</returns>
public byte[] GetOperands(int index, int length)
{
var array = new byte[length];
for(var i=0;i<length;i++)
{
var ind = index + i;
array[i] = GetOperand(ind);
}
return array;
}
/// <summary>
/// Retrieves an ushort from the operand array, starting at index.
/// </summary>
public ushort GetUInt16Operand(int index)
{
return BitConverter.ToUInt16(GetOperands(index,2), 0);
}
/// <summary>
/// Retrieves a short from the operand array, starting at index.
/// </summary>
public short GetInt16Operand(int index)
{
return BitConverter.ToInt16(GetOperands(index, 2), 0);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1bc064226f63fda41bec72257f2ffc1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTS2.Common;
using OpenTS2.Content;
using OpenTS2.Files.Formats.DBPF;
using OpenTS2.Files.Utils;
using UnityEngine;
namespace OpenTS2.SimAntics
{
/// <summary>
/// Parses a BHAV resource into an asset.
/// </summary>
/// https://modthesims.info/wiki.php?title=42484156
[Codec(TypeIDs.BHAV)]
public class BHAVCodec : AbstractCodec
{
public override AbstractAsset Deserialize(byte[] bytes, ResourceKey tgi, DBPFFile sourceFile)
{
var asset = new BHAVAsset();
var stream = new MemoryStream(bytes);
var reader = IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN);
// Header
asset.FileName = reader.ReadNullTerminatedUTF8();
reader.Seek(SeekOrigin.Begin, 64);
var magic = reader.ReadUInt16();
Debug.Assert(magic <= 0x8009);
var instructionCount = reader.ReadUInt16();
var type = reader.ReadByte();
asset.ArgumentCount = reader.ReadByte();
asset.LocalCount = reader.ReadByte();
var flags = reader.ReadByte();
var treeVersion = reader.ReadUInt32();
byte cacheFlags = 0;
// Sims wiki says cacheflags are at the end of each node, not in the header, which isn't correct.
if (magic >= 0x8009)
cacheFlags = reader.ReadByte();
// Nodes
for (var i=0;i<instructionCount;i++)
{
byte nodeVersion = 0;
ushort opcode, trueTarget, falseTarget;
byte[] operands;
if (magic <= 0x8002)
{
opcode = reader.ReadUInt16();
trueTarget = reader.ReadByte();
falseTarget = reader.ReadByte();
operands = reader.ReadBytes(8);
}
else if (magic <= 0x8004)
{
opcode = reader.ReadUInt16();
trueTarget = reader.ReadByte();
falseTarget = reader.ReadByte();
operands = reader.ReadBytes(16);
}
else if (magic <= 0x8006)
{
opcode = reader.ReadUInt16();
trueTarget = reader.ReadByte();
falseTarget = reader.ReadByte();
nodeVersion = reader.ReadByte();
operands = reader.ReadBytes(16);
}
else
{
opcode = reader.ReadUInt16();
trueTarget = reader.ReadUInt16();
falseTarget = reader.ReadUInt16();
nodeVersion = reader.ReadByte();
operands = reader.ReadBytes(16);
}
// Convert from TS1 to TS2 format if necessary.
trueTarget = ParseTarget(trueTarget);
falseTarget = ParseTarget(falseTarget);
var node = new BHAVAsset.Node
{
OpCode = opcode,
TrueTarget = trueTarget,
FalseTarget = falseTarget,
Operands = operands,
Version = nodeVersion
};
asset.Nodes.Add(node);
}
stream.Dispose();
reader.Dispose();
return asset;
ushort ParseTarget(ushort target)
{
switch(target)
{
// None / Error
case 0xFD:
return BHAVAsset.Node.ErrorReturnValue;
// True
case 0xFE:
return BHAVAsset.Node.TrueReturnValue;
// False
case 0xFF:
return BHAVAsset.Node.FalseReturnValue;
// probably already an okay value
default:
return target;
}
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1991f3f661f4f854096da0e28958370c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 456765861d2c2ab4384fc8a8de928bac
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics.Primitives
{
/// <summary>
/// Expression Primitive, handles variable assignation, retrieval, math.
/// </summary>
public class VMExpression : VMPrimitive
{
public enum Operator : byte
{
GreaterThan,
LessThan,
EqualTo,
Add,
Subtract,
Assign,
Multiply,
Divide,
IsFlagSet,
SetFlag,
ClearFlag,
IncThenLessThan,
Modulo,
And,
GreaterThanOrEqualTo,
LessThanOrEqualTo,
NotEqualTo,
DecThenGreaterThan,
Or,
Xor,
Abs,
Assign32BitValue
}
public override VMReturnValue Execute(VMContext ctx)
{
var lhsData = ctx.Node.GetInt16Operand(0);
var rhsData = ctx.Node.GetInt16Operand(2);
var signedFlag = ctx.Node.Operands[4];
var op = (Operator)ctx.Node.Operands[5];
var lhsSource = (VMDataSource)ctx.Node.Operands[6];
var rhsSource = (VMDataSource)ctx.Node.Operands[7];
short lhs, rhs;
switch(op)
{
case Operator.GreaterThan:
return ctx.GetData(lhsSource, lhsData) > ctx.GetData(rhsSource, rhsData) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.LessThan:
return ctx.GetData(lhsSource, lhsData) < ctx.GetData(rhsSource, rhsData) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.EqualTo:
return ctx.GetData(lhsSource, lhsData) == ctx.GetData(rhsSource, rhsData) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.Add:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
ctx.SetData(lhsSource, lhsData, (short)(lhs + rhs));
return VMReturnValue.ReturnTrue;
case Operator.Subtract:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
ctx.SetData(lhsSource, lhsData, (short)(lhs - rhs));
return VMReturnValue.ReturnTrue;
case Operator.Assign:
rhs = ctx.GetData(rhsSource, rhsData);
ctx.SetData(lhsSource, lhsData, rhs);
return VMReturnValue.ReturnTrue;
case Operator.Multiply:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
ctx.SetData(lhsSource, lhsData, (short)(lhs * rhs));
return VMReturnValue.ReturnTrue;
case Operator.Divide:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
ctx.SetData(lhsSource, lhsData, (short)(lhs / rhs));
return VMReturnValue.ReturnTrue;
case Operator.IsFlagSet:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
return ((lhs & (1 << (rhs - 1))) > 0) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.SetFlag:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var bitval = 1 << (rhs - 1);
var finalSet = (int)lhs | bitval;
ctx.SetData(lhsSource, lhsData, (short)(finalSet));
return VMReturnValue.ReturnTrue;
case Operator.ClearFlag:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var clearBitVal = ~(1 << (rhs - 1));
var finalClear = (int)lhs & clearBitVal;
ctx.SetData(lhsSource, lhsData, (short)(finalClear));
return VMReturnValue.ReturnTrue;
case Operator.IncThenLessThan:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var lhsAdded = lhs + 1;
ctx.SetData(lhsSource, lhsData, (short)lhsAdded);
return lhsAdded < rhs ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.Modulo:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var lhsMod = lhs % rhs;
ctx.SetData(lhsSource, lhsData, (short)lhsMod);
return VMReturnValue.ReturnTrue;
case Operator.And:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var lhsAnd = lhs & rhs;
ctx.SetData(lhsSource, lhsData, (short)lhsAnd);
return VMReturnValue.ReturnTrue;
case Operator.GreaterThanOrEqualTo:
return ctx.GetData(lhsSource, lhsData) >= ctx.GetData(rhsSource, rhsData) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.LessThanOrEqualTo:
return ctx.GetData(lhsSource, lhsData) <= ctx.GetData(rhsSource, rhsData) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.NotEqualTo:
return ctx.GetData(lhsSource, lhsData) != ctx.GetData(rhsSource, rhsData) ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.DecThenGreaterThan:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var lhsSubbed = lhs - 1;
ctx.SetData(lhsSource, lhsData, (short)lhsSubbed);
return lhsSubbed > rhs ?
VMReturnValue.ReturnTrue : VMReturnValue.ReturnFalse;
case Operator.Or:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var lhsOr = lhs | rhs;
ctx.SetData(lhsSource, lhsData, (short)lhsOr);
return VMReturnValue.ReturnTrue;
case Operator.Xor:
lhs = ctx.GetData(lhsSource, lhsData);
rhs = ctx.GetData(rhsSource, rhsData);
var lhsXor = lhs ^ rhs;
ctx.SetData(lhsSource, lhsData, (short)lhsXor);
return VMReturnValue.ReturnTrue;
case Operator.Abs:
rhs = ctx.GetData(rhsSource, rhsData);
ctx.SetData(lhsSource, lhsData, Math.Abs(rhs));
return VMReturnValue.ReturnTrue;
// TODO: This is new in TS2 so I'm not sure if this is correct.
case Operator.Assign32BitValue:
rhs = ctx.GetData(rhsSource, rhsData);
var rhs2 = ctx.GetData(rhsSource, (short)(rhsData+1));
ctx.SetData(lhsSource, lhsData, rhs);
ctx.SetData(lhsSource, (short)(lhsData+1), rhs2);
return VMReturnValue.ReturnTrue;
}
return VMReturnValue.ReturnFalse;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 66c1d2b5188323d4ebb04f6fc5f499e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics.Primitives
{
public class VMNotifyStackObjectOutOfIdle : VMPrimitive
{
public override VMReturnValue Execute(VMContext ctx)
{
var stackObject = ctx.VM.GetEntityByID(ctx.StackFrame.StackObjectID);
if (stackObject == null)
throw new KeyNotFoundException($"Couldn't find Object with ID {ctx.StackFrame.StackObjectID}");
ctx.VM.Scheduler.ScheduleInterrupt(ctx.StackObjectEntity.Stack);
return VMReturnValue.ReturnTrue;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cde7a92de8623c40a7dbf0f9527174b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics.Primitives
{
public static class VMPrimitiveRegistry
{
private static Dictionary<ushort, VMPrimitive> s_primitiveByOpCode = new Dictionary<ushort, VMPrimitive>();
public static void Initialize()
{
RegisterPrimitive<VMSleep>(0x0);
RegisterPrimitive<VMExpression>(0x2);
RegisterPrimitive<VMRandomNumber>(0x8);
RegisterPrimitive<VMRemoveObjectInstance>(0x12);
RegisterPrimitive<VMNotifyStackObjectOutOfIdle>(0x31);
}
public static void RegisterPrimitive<T>(ushort opcode) where T : VMPrimitive
{
s_primitiveByOpCode[opcode] = Activator.CreateInstance(typeof(T)) as VMPrimitive;
}
public static T GetPrimitive<T>(ushort opcode) where T : VMPrimitive
{
return GetPrimitive(opcode) as T;
}
public static VMPrimitive GetPrimitive(ushort opcode)
{
if (s_primitiveByOpCode.TryGetValue(opcode, out VMPrimitive returnPrim))
return returnPrim;
return null;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2251550a13386fc4ca77428e52e11359
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics.Primitives
{
public class VMRandomNumber : VMPrimitive
{
public override VMReturnValue Execute(VMContext ctx)
{
var lhsSource = (VMDataSource)ctx.Node.GetOperand(2);
var lhsData = ctx.Node.GetInt16Operand(0);
var rhsSource = (VMDataSource)ctx.Node.GetOperand(6);
var rhsData = ctx.Node.GetInt16Operand(4);
var randomMaxValue = ctx.GetData(rhsSource, rhsData);
var randomFinalValue = (short)UnityEngine.Random.Range(0, randomMaxValue);
ctx.SetData(lhsSource, lhsData, randomFinalValue);
return VMReturnValue.ReturnTrue;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f67753d3b8a97c442b581b3a03d97f53
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTS2.Common;
namespace OpenTS2.SimAntics.Primitives
{
public class VMRemoveObjectInstance : VMPrimitive
{
public override VMReturnValue Execute(VMContext ctx)
{
// These two seem to be a mystery according to FreeSO src
var returnImmediately = ((ctx.Node.GetOperand(2) & 1) == 1);
var cleanUpAll = ((ctx.Node.GetOperand(2) & 2) != 2);
var entityToRemove = ctx.Node.GetOperand(0) > 0 ? ctx.StackObjectEntity : ctx.Entity;
entityToRemove.Delete();
// FreeSO yields 1 tick in the case of self deletion, preventing further execution of the script, so we also do that.
// TS2 BHAVs tend to remove themselves -> idle for a few ticks -> loop back to remove prim.
if (entityToRemove == ctx.Entity)
return VMReturnValue.ReturnTrueNextTick;
return VMReturnValue.ReturnTrue;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f4f2d62be2b411b43b4fb2ab0347a435
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics.Primitives
{
/// <summary>
/// Sleeps the current thread for a number of ticks.
/// </summary>
public class VMSleep : VMPrimitive
{
public override VMReturnValue Execute(VMContext ctx)
{
var argumentIndex = ctx.Node.GetUInt16Operand(0);
var sleepTicks = (uint)Math.Max(0,(int)ctx.StackFrame.Arguments[argumentIndex]);
return new VMReturnValue(new ContinueHandler(ctx.Stack, sleepTicks));
}
/// <summary>
/// Handles VM thread blocking for the Sleep prim.
/// </summary>
public class ContinueHandler : VMContinueHandler
{
public uint TargetTick = 0;
VMStack _stack;
public ContinueHandler(VMStack stack, uint ticks)
{
_stack = stack;
var vm = _stack.Entity.VM;
TargetTick = vm.CurrentTick + ticks;
}
public override VMExitCode Tick()
{
if (_stack.Interrupt)
{
// Handled!
_stack.Interrupt = false;
return VMExitCode.True;
}
if (_stack.Entity.VM.CurrentTick >= TargetTick)
return VMExitCode.True;
return VMExitCode.Continue;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b295e337c8206e43bfa5c8ba0fc3fb5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,82 @@
using OpenTS2.Common;
using OpenTS2.Content;
using OpenTS2.Files.Formats.DBPF;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace OpenTS2.SimAntics
{
/// <summary>
/// SimAntics virtual machine.
/// </summary>
public class VM
{
public VMScheduler Scheduler = new VMScheduler();
public List<VMEntity> Entities = new List<VMEntity>();
public uint CurrentTick = 0;
private Dictionary<short, VMEntity> _entitiesByID = new Dictionary<short, VMEntity>();
/// <summary>
/// Ticks all entities and advances the Simulation by 1 tick.
/// </summary>
public void Tick()
{
Scheduler.OnBeginTick(this);
foreach(var entity in Entities)
{
entity.Tick();
}
Scheduler.OnEndTick(this);
CurrentTick++;
}
/// <summary>
/// Retrieves a BHAV Asset from the content system.
/// </summary>
public static BHAVAsset GetBHAV(ushort id, uint groupID)
{
return ContentProvider.Get().GetAsset<BHAVAsset>(new ResourceKey(id, groupID, TypeIDs.BHAV));
}
public VMEntity GetEntityByID(short id)
{
if (_entitiesByID.TryGetValue(id, out VMEntity result))
return result;
return null;
}
/// <summary>
/// Adds an entity to the simulator, and assigns a unique ID to it.
/// </summary>
/// <param name="entity"></param>
public void AddEntity(VMEntity entity)
{
entity.VM = this;
entity.ID = GetUniqueID();
Entities.Add(entity);
_entitiesByID[entity.ID] = entity;
}
public void RemoveEntity(short id)
{
if (!_entitiesByID.TryGetValue(id, out VMEntity result))
return;
_entitiesByID.Remove(id);
Entities.Remove(result);
}
/// <summary>
/// Returns a free unused entity ID.
/// </summary>
public short GetUniqueID()
{
short resultID = 1;
while (_entitiesByID.TryGetValue(resultID, out VMEntity _))
resultID++;
return resultID;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 41af7919700444842831300feb1548fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
/// <summary>
/// SimAntics virtual machine context to be sent to primitives and such.
/// </summary>
public struct VMContext
{
public VMStackFrame StackFrame;
public BHAVAsset.Node Node;
public VMStack Stack => StackFrame.Stack;
public VMEntity Entity => StackFrame.Stack.Entity;
public VMEntity StackObjectEntity => VM.GetEntityByID(StackFrame.StackObjectID);
public VM VM => Entity.VM;
// TODO - still super incomplete, just enough to run basic scripts.
public short GetData(VMDataSource source, short dataIndex)
{
return source switch
{
VMDataSource.Literal => dataIndex,
VMDataSource.Temps => Entity.Temps[dataIndex],
VMDataSource.Params => StackFrame.Arguments[dataIndex],
VMDataSource.StackObjectID => StackFrame.StackObjectID,
VMDataSource.TempByTempIndex => Entity.Temps[Entity.Temps[dataIndex]],
VMDataSource.StackObjectsTemp => StackObjectEntity.Temps[dataIndex],
VMDataSource.Local => StackFrame.Locals[dataIndex],
VMDataSource.StackObjectsDefinition => (short)StackObjectEntity.ObjectDefinition.Fields[dataIndex],
_ => throw new ArgumentOutOfRangeException("SimAntics data source out of range!")
};
}
public void SetData(VMDataSource source, short dataIndex, short value)
{
switch(source)
{
case VMDataSource.Temps:
Entity.Temps[dataIndex] = value;
return;
case VMDataSource.Params:
StackFrame.Arguments[dataIndex] = value;
return;
case VMDataSource.StackObjectID:
StackFrame.StackObjectID = value;
return;
case VMDataSource.TempByTempIndex:
Entity.Temps[Entity.Temps[dataIndex]] = value;
return;
case VMDataSource.StackObjectsTemp:
StackObjectEntity.Temps[dataIndex] = value;
return;
case VMDataSource.Local:
StackFrame.Locals[dataIndex] = value;
return;
}
throw new ArgumentOutOfRangeException("SimAntics data source out of range!");
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 002b800405232b04eb76e97756962bd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
public abstract class VMContinueHandler
{
public abstract VMExitCode Tick();
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80b0d4061c5924a4f86ed92e1b1d4886
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
public enum VMDataSource : byte
{
MyObjectsAttributes,
StackObjectsAttributes,
MyObjectsSemiAttributes,
MyObject,
StackObject,
StackObjectsSemiAttributes,
Globals,
Literal,
Temps,
Params,
StackObjectID,
// Temp[Temp]
TempByTempIndex,
CheckTreeAdRange,
StackObjectsTemp,
MyMotives,
StackObjectsMotives,
StackObjectsSlot,
// Stack obj's motive[temp]
StackObjectsMotiveByTemp,
MyPersonData,
StackObjectsPersonData,
MySlot,
StackObjectsDefinition,
// Stack Objs Attribute[Param]
StackObjectsAttributeByParam,
// Room[Temp0]
RoomInTemp0,
NeighborInStackObject,
Local,
Constant,
Unused,
CheckTreeAdPersonalityVar,
CheckTreeAdMin,
// My Person Data[Temp]
MyPersonDataByTemp,
// Stack Obj's person data [Temp]
StackObjectsPersonDataByTemp,
NeighborsPersonData,
// Job data [temp0,1]
JobDataByTemp0And1,
NeighborhoodDataReadOnly,
StackObjectsFunction,
MyTypeAttribute,
StackObjectsTypeAttribute,
NeighborsDefinition,
MyTempToken,
StackObjectsTempToken,
// My object array [array] Iterator Index
MyObjectsArrayByArrayIteratorIndex,
// Stack Object's object array [array] iterator Index
StackObjectsArrayByArrayIteratorIndex,
// My object array [array] iterator data
MyObjectsArrayByArrayIteratorData,
// Stack Object's object array [array] iterator Data
StackObjectsArrayByArrayIteratorData,
// My object array [array] element at Temp0
MyObjectsArrayByArrayElementAtTemp0,
// Stack Object's object array [array] element at Temp0
StackObjectsArrayByArrayElementAtTemp0,
// Const[Temp]
ConstByTemp,
// My slot[Temp]
MySlotByTemp,
// Stack Object's slot[Temp]
StackObjectsSlotByTemp,
// Stack Object's semi attr[Param]
StackObjectsSemiAttributeByParam,
StackObjectsMasterDefinition
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b936f65f70e77874f8c424e5363af9f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,60 @@
using OpenTS2.Content.DBPF;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
/// <summary>
/// This is a process or thread running in the SimAntics virtual machine, with its own stack and temp variables.
/// </summary>
public class VMEntity
{
public bool PendingDeletion = false;
public short ID = 1;
public short[] Temps = new short[20];
public VMStack Stack;
public VM VM;
public ObjectDefinitionAsset ObjectDefinition;
public uint PrivateGroupID => ObjectDefinition.GlobalTGI.GroupID;
public uint SemiGlobalGroupID
{
get
{
var semiGlobal = ObjectDefinition.SemiGlobal;
if (semiGlobal == null)
return 0;
return semiGlobal.SemiGlobalGroupID;
}
}
protected VMEntity()
{
Stack = new VMStack(this);
}
public VMEntity(ObjectDefinitionAsset objectDefinition) : this()
{
ObjectDefinition = objectDefinition;
}
public void Tick()
{
Stack.Tick();
}
public void Delete()
{
if (VM.Scheduler.RunningTick)
{
if (PendingDeletion)
return;
VM.Scheduler.ScheduleDeletion(this);
return;
}
VM.RemoveEntity(ID);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 17d0bfd1bb2c4ab48a5e03c6c55d4c3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,9 @@
namespace OpenTS2.SimAntics
{
public enum VMExitCode : byte
{
True,
False,
Continue
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e8ea9071ef11a043b50e2f12f622914
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
public abstract class VMPrimitive
{
public abstract VMReturnValue Execute(VMContext ctx);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 636b30d1f9f76fa4b8ff19785fe663f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
public struct VMReturnValue
{
public VMExitCode Code;
public VMContinueHandler ContinueHandler;
public static VMReturnValue ReturnTrue = new VMReturnValue(VMExitCode.True);
public static VMReturnValue ReturnFalse = new VMReturnValue(VMExitCode.False);
public static VMReturnValue ReturnTrueNextTick = new VMReturnValue(new YieldOneTickContinueHandler(VMExitCode.True));
public static VMReturnValue ReturnFalseNextTick = new VMReturnValue(new YieldOneTickContinueHandler(VMExitCode.False));
public VMReturnValue(VMExitCode exitCode)
{
Code = exitCode;
ContinueHandler = null;
}
public VMReturnValue(VMContinueHandler continueHandler)
{
Code = VMExitCode.Continue;
ContinueHandler = continueHandler;
}
public class YieldOneTickContinueHandler : VMContinueHandler
{
bool _ticked = false;
VMExitCode _exitCode;
public YieldOneTickContinueHandler(VMExitCode exitCode)
{
_exitCode = exitCode;
}
public override VMExitCode Tick()
{
if (!_ticked)
{
_ticked = true;
return VMExitCode.Continue;
}
return _exitCode;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f827d2fe75bd932488d54abccaf9fd28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
public class VMScheduler
{
public bool RunningTick => _runningTick;
private bool _runningTick = false;
private VMScheduledEvents _preTickEvents = new VMScheduledEvents();
private VMScheduledEvents _postTickEvents = new VMScheduledEvents();
/// <summary>
/// Runs a function immediately if the VM is not currently in a tick, otherwise schedules it to run at the end of the current tick.
/// </summary>
public void ScheduleWhenPossible(Action func)
{
if (!RunningTick)
{
func.Invoke();
return;
}
ScheduleOnEndTick(func);
}
/// <summary>
/// Schedules a function to run once the VM begins a tick.
/// </summary>
public void ScheduleOnBeginTick(Action func, uint targetTick = 0)
{
_preTickEvents.AddEvent(new VMEvent(func, targetTick));
}
/// <summary>
/// Schedules a function to run once the VM is done with a tick.
/// </summary>
public void ScheduleOnEndTick(Action func, uint targetTick = 0)
{
_postTickEvents.AddEvent(new VMEvent(func, targetTick));
}
// TODO - Right now the way this works is that once an entity has been notified out of idle/interrupted, the first idle that gets executed on the next tick (or that continues to run if there is one already running) won't actually sleep. If no idles are ran then the interrupt is just discarded. Verify this is okay!
public void ScheduleInterrupt(VMStack stack)
{
ScheduleWhenPossible(() =>
{
stack.Interrupt = true;
});
}
public void ScheduleDeletion(VMEntity entity)
{
ScheduleWhenPossible(() =>
{
entity.Delete();
});
entity.PendingDeletion = true;
}
public void OnBeginTick(VM vm)
{
_preTickEvents.Run(vm);
_runningTick = true;
}
public void OnEndTick(VM vm)
{
_runningTick = false;
_postTickEvents.Run(vm);
}
public class VMEvent
{
public Action Event;
public uint TargetTick = 0;
public VMEvent(Action func, uint targetTick = 0)
{
Event = func;
TargetTick = targetTick;
}
}
public class VMScheduledEvents
{
private List<VMEvent> _events = new List<VMEvent>();
public void Run(VM vm)
{
var newEvList = new List<VMEvent>();
foreach(var ev in _events)
{
if (vm.CurrentTick >= ev.TargetTick)
{
ev.Event?.Invoke();
}
else
newEvList.Add(ev);
}
_events = newEvList;
}
public void AddEvent(VMEvent ev)
{
_events.Add(ev);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 86354629246dece4fac8ccc5660ae5f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
/// <summary>
/// Stack of scripts to run on a SimAntics entity/thread.
/// </summary>
public class VMStack
{
public bool Interrupt = false;
// For check trees and other things that should execute and return immediately we should set this to false.
public bool CanYield = true;
public VMEntity Entity;
public Stack<VMStackFrame> Frames = new Stack<VMStackFrame>();
public VMStack(VMEntity entity)
{
Entity = entity;
}
public VMStackFrame GetCurrentFrame()
{
if (Frames.Count == 0)
return null;
return Frames.Peek();
}
VMExitCode TickInternal()
{
var currentFrame = GetCurrentFrame();
if (currentFrame == null)
return VMExitCode.False;
var returnValue = currentFrame.Tick();
if (returnValue == VMExitCode.Continue && !CanYield)
throw new Exception("Attempted to yield in a non-yielding VMStack.");
while (returnValue != VMExitCode.Continue)
{
Frames.Pop();
currentFrame = GetCurrentFrame();
if (currentFrame == null)
return returnValue;
var currentNode = currentFrame.GetCurrentNode();
if (returnValue == VMExitCode.True)
currentFrame.CurrentNode = currentNode.TrueTarget;
else
currentFrame.CurrentNode = currentNode.FalseTarget;
returnValue = currentFrame.Tick();
}
return returnValue;
}
public VMExitCode Tick()
{
var returnValue = TickInternal();
Interrupt = false;
return returnValue;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d66f3e25b8c58944789a10675ddaa524
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,230 @@
using OpenTS2.Files.Formats.DBPF;
using OpenTS2.SimAntics.Primitives;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTS2.SimAntics
{
/// <summary>
/// A frame in a SimAntics stack.
/// </summary>
public class VMStackFrame
{
public VMStack Stack;
public BHAVAsset BHAV;
public short StackObjectID = 0;
public int CurrentNode = 0;
/// <summary>
/// Current blocking behavior. As long as this variable's Tick() returns Continue this thread won't move.
/// </summary>
public VMContinueHandler CurrentContinueHandler = null;
public short[] Locals;
public short[] Arguments;
public VMStackFrame(BHAVAsset bhav, VMStack stack)
{
BHAV = bhav;
Stack = stack;
Locals = new short[BHAV.LocalCount];
Arguments = new short[BHAV.ArgumentCount];
}
public VMExitCode Tick()
{
if (CurrentContinueHandler != null)
{
var returnCode = CurrentContinueHandler.Tick();
if (returnCode == VMExitCode.Continue)
return returnCode;
else
{
return AdvanceNodeAndRunTick(returnCode);
}
}
return RunCurrentTick();
}
VMExitCode AdvanceNodeAndRunTick(VMExitCode exitCode)
{
var currentNode = GetCurrentNode();
ushort returnTarget;
if (exitCode == VMExitCode.True)
returnTarget = currentNode.TrueTarget;
else
returnTarget = currentNode.FalseTarget;
switch (returnTarget)
{
case BHAVAsset.Node.FalseReturnValue:
return VMExitCode.False;
case BHAVAsset.Node.TrueReturnValue:
return VMExitCode.True;
case BHAVAsset.Node.ErrorReturnValue:
throw new Exception("Jumped to Error.");
default:
SetCurrentNode(returnTarget);
return RunCurrentTick();
}
}
VMExitCode RunCurrentTick()
{
var currentNode = GetCurrentNode();
if (currentNode != null)
{
var context = new VMContext
{
StackFrame = this,
Node = currentNode
};
var opcode = currentNode.OpCode;
var prim = VMPrimitiveRegistry.GetPrimitive(opcode);
if (prim != null)
{
var primReturn = prim.Execute(context);
if (primReturn.Code == VMExitCode.Continue)
primReturn.Code = primReturn.ContinueHandler.Tick();
if (primReturn.Code == VMExitCode.Continue)
{
CurrentContinueHandler = primReturn.ContinueHandler;
return primReturn.Code;
}
else
{
CurrentContinueHandler = null;
return AdvanceNodeAndRunTick(primReturn.Code);
}
}
else
{
var newStackFrame = CreateStackFrameForNode(context);
if (newStackFrame != null)
{
Stack.Frames.Push(newStackFrame);
return newStackFrame.Tick();
}
}
}
return VMExitCode.False;
}
enum GoSubFormat
{
PassTemps,
TS1,
TS2,
CallerParams
}
VMStackFrame CreateStackFrameForNode(VMContext ctx)
{
var bhav = GetBHAVForOpCode(ctx.Node.OpCode);
if (bhav == null)
return null;
var newStackFrame = new VMStackFrame(bhav, Stack);
newStackFrame.StackObjectID = ctx.StackFrame.StackObjectID;
GoSubFormat format = GoSubFormat.PassTemps;
if (ctx.Node.GetOperand(12) > 0)
{
format = GoSubFormat.TS2;
if (ctx.Node.GetOperand(12) == 2 && ctx.Node.Version > 0)
format = GoSubFormat.CallerParams;
}
else
{
for (var i = 0; i < 8; i++)
{
if (ctx.Node.Operands[i] != 0xFF)
{
format = GoSubFormat.TS1;
break;
}
}
}
var argAmount = 0;
switch(format)
{
case GoSubFormat.PassTemps:
argAmount = Math.Min(newStackFrame.Arguments.Length, Stack.Entity.Temps.Length);
for (var i=0;i<argAmount;i++)
{
newStackFrame.TrySetArgument(i, Stack.Entity.Temps[i]);
}
break;
case GoSubFormat.TS1:
argAmount = Math.Min(newStackFrame.Arguments.Length, 8);
for(var i=0;i<argAmount;i++)
{
newStackFrame.TrySetArgument(i, ctx.Node.GetInt16Operand(i*2));
}
break;
case GoSubFormat.TS2:
argAmount = Math.Min(newStackFrame.Arguments.Length, 4);
for(var i=0;i<argAmount;i++)
{
var dataSourceIndex = i * 3;
var dataValueIndex = dataSourceIndex + 1;
var dataSource = (VMDataSource)ctx.Node.GetOperand(dataSourceIndex);
var dataValue = ctx.Node.GetInt16Operand(dataValueIndex);
var data = ctx.GetData(dataSource, dataValue);
newStackFrame.TrySetArgument(i, data);
}
break;
case GoSubFormat.CallerParams:
argAmount = Math.Min(newStackFrame.Arguments.Length, ctx.StackFrame.Arguments.Length);
for (var i=0;i<argAmount;i++)
{
newStackFrame.TrySetArgument(i, ctx.StackFrame.Arguments[i]);
}
break;
}
return newStackFrame;
}
void TrySetArgument(int index, short value)
{
if (index < 0)
return;
if (Arguments.Length <= index)
return;
Arguments[index] = value;
}
BHAVAsset GetBHAVForOpCode(ushort opCode)
{
// 0x0XXX is global scope, 0x1XXX is private scope and 0x2XXX is semiglobal scope.
var groupid = Stack.Entity.SemiGlobalGroupID;
if (opCode < 0x1000)
groupid = GroupIDs.Global;
else if (opCode < 0x2000)
groupid = Stack.Entity.PrivateGroupID;
return VM.GetBHAV(opCode, groupid);
}
public void SetCurrentNode(int nodeIndex)
{
CurrentNode = nodeIndex;
CurrentContinueHandler = null;
}
public BHAVAsset.Node GetCurrentNode()
{
return BHAV.Nodes[CurrentNode];
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f0131025319ce348a70d3ee3ee46cb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Assets/Tests/OpenTS2/SimAntics.meta generated Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9fc25c06c3b7e794c9c8694d4dc67ff3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using OpenTS2.Content;
using OpenTS2.SimAntics;
using OpenTS2.Common;
using OpenTS2.Files.Formats.DBPF;
using OpenTS2.SimAntics.Primitives;
using OpenTS2.Content.DBPF;
public class SimAnticsTest
{
private uint _groupID;
[SetUp]
public void SetUp()
{
TestMain.Initialize();
_groupID = ContentProvider.Get().AddPackage("TestAssets/SimAntics/bhav.package").GroupID;
}
[Test]
public void TestLoadsBHAV()
{
var bhav = VM.GetBHAV(0x1001, _groupID);
Assert.That(bhav.FileName, Is.EqualTo("OpenTS2 BHAV Test"));
Assert.That(bhav.ArgumentCount, Is.EqualTo(1));
Assert.That(bhav.LocalCount, Is.EqualTo(0));
}
[Test]
public void TestRunBHAV()
{
// VM Entities need to be attached to an OBJD to be aware of private/semiglobal scope.
var testObjectDefinition = new ObjectDefinitionAsset();
testObjectDefinition.TGI = new ResourceKey(1, _groupID, TypeIDs.OBJD);
var bhav = VM.GetBHAV(0x1001, _groupID);
var vm = new VM();
var entity = new VMEntity(testObjectDefinition);
vm.AddEntity(entity);
var stackFrame = new VMStackFrame(bhav, entity.Stack);
entity.Stack.Frames.Push(stackFrame);
// Test BHAV:
// Multiplies Param0 by 2, stores it in Temp0
// Sleeps for 1 Tick
// Sets Temp0 to 1200
// Sleeps for 20000 Ticks
// Sets Temp0 to 0
stackFrame.Arguments[0] = 10;
vm.Tick();
Assert.That(entity.Temps[0], Is.EqualTo(20));
vm.Tick();
Assert.That(entity.Temps[0], Is.EqualTo(1200));
// Interrupt idle here, so that it doesn't sleep for 20000 ticks.
vm.Scheduler.ScheduleInterrupt(entity.Stack);
vm.Tick();
Assert.That(entity.Temps[0], Is.EqualTo(0));
}
[Test]
public void TestPrimitiveRegistry()
{
var vmExpressionPrim = VMPrimitiveRegistry.GetPrimitive(0x2);
Assert.That(vmExpressionPrim, Is.TypeOf(typeof(VMExpression)));
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dff9cc352fdef4a4fa14c911aa68b29a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -3,6 +3,7 @@ using OpenTS2.Client;
using OpenTS2.Content;
using OpenTS2.Files;
using OpenTS2.Files.Formats.DBPF;
using OpenTS2.SimAntics.Primitives;
/// <summary>
/// Main initialization class for OpenTS2 unit testing.
@ -28,6 +29,7 @@ public static class TestMain
Filesystem.Initialize(new TestPathProvider(), epManager);
CodecAttribute.Initialize();
AssemblyHelper.InitializeLoadedAssemblies();
VMPrimitiveRegistry.Initialize();
s_initialized = true;
}

Binary file not shown.