Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 34 additions & 231 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,277 +2,80 @@

[![Test](https://github.com/simulation-tree/simulation/actions/workflows/test.yml/badge.svg)](https://github.com/simulation-tree/simulation/actions/workflows/test.yml)

Library providing a way to organize systems, and executing programs.
Library providing a way to organize and update systems with support for message handling.

### Running simulators

Simulators contain and update systems, and iterate over programs found
in the world that the simulator is created with:
Simulators contain and update systems:
```cs
public static int Main()
public static void Main()
{
StatusCode statusCode;
using (World world = new())
{
using (Simulator simulator = new(world))
{
simulator.AddSystem(new AllSystems());
using (Program program = Program.Create(world, new ExampleProgram(100)))
{
while (!program.IsFinished(out statusCode))
{
simulator.Update();
}

Console.WriteLine(program.Read<ExampleProgram>().value);
}

simulator.RemoveSystem<AllSystems>();
}
}
using World world = new();
using Simulator simulator = new(world);

return statusCode.IsSuccess ? 0 : 1;
simulator.Add(new ProgramSystems());
simulator.Update(world);
simulator.Remove<ProgramSystems>();
}
```

### Order of operations

Systems take precedence over programs:

**Starting and Updating**

Systems will always be started and updated before programs. Iterating with the simulator
world first, and then with each program world in the order they were created.

**Finishing**

Same as starting/updating but flipped. Systems finish before programs are, and with
program worlds in reverse order, then with simulator world last.

**Messages**

When messages are given to a simulator to handle, systems will be called with the
simulator world, and then program worlds last.

### Systems

Systems are defined by implementing the `ISystem` interface, and are responsible
for initializing, updating, and finalizing the system with all worlds that the
simulator is aware of.

```cs
public readonly partial struct ExampleSystem : ISystem
public class ProgramSystems : ISystem, IDisposable
{
void IDisposable.Dispose()
public ProgramSystems()
{
//initialize
}

void ISystem.Start(in SystemContext context, in World world)
public void Dispose()
{
//clean up
}

void ISystem.Update(in SystemContext context, in World world, in TimeSpan delta)
{
}

void ISystem.Finish(in SystemContext context, in World world)
void ISystem.Update(Simulator simulator, double deltaTime)
{
//do work
}
}
```

### Initializing systems with dependent data
### Receiving messages

When systems are added to a simulator world, their constructor can be used to customize
the initial data. In the case that they need information from the simulator itself, the
state of the system can be overwritten using the `Write()` function in `Start()`:
A system that is partial, and implementing the `IListener<T>` interface will
allow it to receive messages broadcast by the simulator:
```cs
public readonly partial struct ExampleSystem : ISystem
{
private readonly int initialData;
private readonly World simulatorWorld;
private readonly List<double> numbers;

[Obsolete("Default constructor not supported", true)]
public ExampleSystem() { } //doing this to enforce the public constructor below
simulator.Broadcast(32f);
simulator.Broadcast(32f);
simulator.Broadcast(32f);

public ExampleSystem(int initialData)
{
this.initialData = initialData;
}

private ExampleSystem(int initialData, World simulatorWorld)
{
this.initialData = initialData;
this.simulatorWorld = simulatorWorld;
numbers = new();
}

void ISystem.Start(in SystemContext context, in World world)
{
if (context.IsSimulatorWorld(world))
{
context.Write(new ExampleSystem(initialData, context.SimulatorWorld));
}
}

...
}
```

### Sub systems

When a system intends to have sub systems, and to act as a high level wrapper.
The `Start()` and `Finish()` functions are used to add and remove the sub systems:
```cs
public readonly partial struct AllSystems : ISystem
public partial class ListenerSystem : IListener<float>
{
void IDisposable.Dispose()
{
}

void ISystem.Start(in SystemContext context, in World world)
{
if (context.IsSimulatorWorld(world))
{
context.AddSystem(new ExampleSystem(100));
}
}

void ISystem.Update(in SystemContext context, in World world, in TimeSpan delta)
void IListener<float>.Receive(ref float message)
{
}

void ISystem.Finish(in SystemContext context, in World world)
{
if (context.IsSimulatorWorld(world))
{
context.RemoveSystem<ExampleSystem>();
}
//do something with this
}
}
```

Notice that the systems aren't added/removed in the constructor/dispose. But instead,
they're added through the start/finish. This is because the `SystemContext` has functionality
for checking if initializing world is the simulator world, which will always be first and last.

### Programs

Programs are entities created in the simulator's world, and are defined by implementing
the `IProgram` interface. They expose functions that are called by the simulator, and
contain their own world separate from the simulator's:
Messages can also be broadcast by reference, allowing systems to modify them,
and use it to communicate between different projects:
```cs
public partial struct ExampleProgram : IProgram<ExampleProgram>
{
public readonly int initialValue;
public int value;

public ExampleProgram(int initialValue)
{
this.initialValue = initialValue;
value = initialValue;
}

void IProgram<ExampleProgram>.Start(ref ExampleProgram program, in Simulator simulator, in World world)
{
}

StatusCode IProgram<ExampleProgram>.Update(in TimeSpan delta)
{
if (value > 200)
{
return StatusCode.Success(0);
}

value++;
return StatusCode.Continue;
}
LoadRequest request = new();
simulator.Broadcast(ref request);
Assert.That(request.loaded, Is.True);

void IProgram<ExampleProgram>.Finish(in StatusCode statusCode)
{
}
}
```

### Finishing programs

A program is finished when the status code from `Update()` is neither `default` or `StatusCode.Continue`.
This status code will then be available in `Finish()`. If the program is finished because
the simulator is disposed before the program decides to, the status code will be `StatusCode.Termination`.

### Message handling

Systems can also be message handlers. This is done by implementing the `ISystem.CollectMessageHandlers()`
function, and then adding all of the handler functions for each type of message:

```cs
public struct StoreNumber
public partial class LoadSystem : IListener<LoadRequest>
{
public double number;

public StoreNumber(double number)
void IListener<float>.Receive(ref LoadRequest message)
{
this.number = number;
message.loaded = true;
}
}

public readonly struct ExampleSystem : ISystem
public struct LoadRequest
{
unsafe readonly void ISystem.CollectMessageHandlers(MessageHandlerCollector collectors)
{
collectors.Add<StoreNumber>(&HandleStoreNumber);
}

[UnmanagedCallersOnly]
private static StatusCode HandleStoreNumber(HandleMessage.Input input)
{
ref StoreNumber message = ref input.ReadMessage<StoreNumber>();
if (message.number < 0)
{
return StatusCode.Failure(0); //cant store negative numbers
}
else
{
ref ExampleSystem system = ref input.ReadSystem<ExampleSystem>();
system.numbers.Add(message.number);
return StatusCode.Success(0); //successfully stored the number
}
}
public bool loaded;
}
```

The intended return values are:
- `StatusCode.Continue`: The message was received, but no handling was done
- `StatusCode.Success`: Message was handled and completed the objective
- `StatusCode.Failure`: Message was handled and attempting to complete it failed
- `default`: Reserved to indicate that the message was not received by the system

### Message requesting

Assuming there's a system that handles `StoreNumber` and none to handle `double`
messages:
```cs
//positive numbers do get added
StatusCode result = contextOrSimulator.TryHandleMessage(new StoreNumber(13.37));
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.IsFailure, Is.False);

//negative numbers not allowed
result = contextOrSimulator.TryHandleMessage(new StoreNumber(-1.0));
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.IsFailure, Is.True);

//when no system is registered to handle the message type of double
result = contextOrSimulator.TryHandleMessage(13.37);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.IsFailure, Is.False);
Assert.That(result == default, Is.True);
```

Messages can also be passed by referenced in case the message contains
mutable data.

### Contributing and design

This library is created for composing behaviour of programs using systems, ideally created by
Expand Down
Loading