RoguelikeToolkit.Entities 5.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package RoguelikeToolkit.Entities --version 5.0.0
                    
NuGet\Install-Package RoguelikeToolkit.Entities -Version 5.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RoguelikeToolkit.Entities" Version="5.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RoguelikeToolkit.Entities" Version="5.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RoguelikeToolkit.Entities" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RoguelikeToolkit.Entities --version 5.0.0
                    
#r "nuget: RoguelikeToolkit.Entities, 5.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RoguelikeToolkit.Entities@5.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RoguelikeToolkit.Entities&version=5.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RoguelikeToolkit.Entities&version=5.0.0
                    
Install as a Cake Tool

RoguelikeTools.Entities

This is a library to allow resolve entity templates for Entity Component System in a similar way Ultimate Adom is doing.

Supports entity template inheritance and creation of entity hierarchies.
It is using the awesome DefaultEcs library to construct the entities.

Features

  • YAML and JSON support: Entity templates can be written in human-readable .yaml or .json formats.
  • Inheritance: Templates can inherit from one or more base templates, mixing components and tags together.
  • Embedded templates: Hierarchies are fully supported, you can embed child entities inside templates.
  • Scripts: Components can carry JavaScript (Jint) scripts for data-driven behavior; parsed once, shared, AOT-safe.
  • Dice parsing: The system seamlessly creates component variables from standard dice notations (2d6+3).

Quick Start

1. Define Templates

Create a file named templates.yaml in your project containing your entity definitions:

# A base template with generic components
EnemyBase:
  Tags:
    - AI
    - CanMove
  Components:
    Health:
      Max: 10
      Current: 10
    Speed:
      Value: 5

# A concrete template that inherits the EnemyBase
Goblin:
  Inherits:
    - EnemyBase
  Tags:
    - Goblin
  Components:
    Attack:
      Damage: "1d4" # This will automatically be parsed into a Dice object!
    Health:
      Max: 15 # Overrides the EnemyBase's Health Max value

2. Loading Templates & Creating Entities

Load these templates and generate DefaultEcs entities easily:

using DefaultEcs;
using RoguelikeToolkit.Entities.Repository;
using RoguelikeToolkit.Entities.Factory;

var world = new World();
var repository = new EntityTemplateRepository();

// Load the templates
repository.LoadTemplateFolder("path/to/my/templates");

// Initialize the factory
var factory = new EntityFactory(repository, world);

// Create the entity!
if (factory.TryCreate("Goblin", out Entity goblinEntity))
{
    // The goblinEntity now has:
    // - Health component (Max: 15, Current: 0) -- inheritance overrides whole components, not single fields
    // - Speed component (Value: 5)
    // - Attack component (Damage: 1d4)
    // - Tags: "AI", "CanMove", "Goblin"
}

Advanced Examples

Embedded Entities

You can embed child entities into another template. When EntityFactory creates an OrcRider, it also creates the embedded Wolf and automatically assigns it as a child using DefaultEcs.Entity.SetAsParentOf.

Wolf:
  Components:
    Speed:
      Value: 10

OrcRider:
  Components:
    Mount:
      Type: "Wolf"
  # Embeds the 'Wolf' template
  Wolf: {}

Querying Templates

repository.TryGetByName("Goblin", out var template); // O(1), case-insensitive

// Tag index: cost follows the matches, not the repository size.
// Requires ALL tags; an empty query returns every template.
var meleeEnemies = repository.GetByTags("AI", "CanMove");

Spawning at Scale

Inheritance is resolved once per template and cached, so spawning thousands of instances of the same blueprint stays flat:

for (var i = 0; i < 2000; i++)
{
    factory.TryCreate("Goblin", out var goblin);
}

Global components are first-write-wins: the first spawned entity sets the world's shared copy, later spawns link to it. Dice strings ("1d4") share one parse per distinct expression; scripts likewise share one parsed, immutable instance per distinct source.

Scripts

Script-typed component members (EntityScript, EntityComponentScript, EntityInteractionScript, Script, all in RoguelikeToolkit.Entities.Scripting) are populated from template strings:

Components:
  Behavior:
    OnHit: |
      var h = entity.GetComponent(Health);
      h.Current -= damage;
      entity.SetComponent(Health, h);
    Regen: component.Current += amount;
[Component]
public class Behavior
{
    public EntityScript? OnHit { get; set; }
    public EntityComponentScript? Regen { get; set; }
}

// one host per thread / simulation loop; it owns the (single) Jint engine
using var host = new ScriptHost(new ScriptHostOptions { ReferenceTypes = new[] { typeof(Health) } });

var behavior = goblin.Get<Behavior>();
behavior.OnHit!.ExecuteOn(host, ref goblin, ("damage", 4));
behavior.Regen!.TryExecuteOn<Health>(host, goblin, ("amount", 2)); // writes the component back via Entity.Set
  • Scripts are immutable and hold no state: each distinct source is parsed once per process and the instance is shared by every entity spawned from the template. Keep state in components.
  • Each execution runs in its own function scope (var/function do not leak between scripts, return exits early); bindings (entity, component, source/target, arguments) are cleared afterwards.
  • entity.HasComponent/GetComponent/SetComponent/RemoveComponent(Type) take any type exposed via ScriptHostOptions.ReferenceTypes. Value-type components come back as copies: write them back with SetComponent.
  • Limits: 500 statements and recursion depth 1024 per execution by default; Timeout and MemoryLimitBytes are opt-in.
  • ScriptHost is not thread-safe (neither is a Jint engine); scripts are, and can run on any number of hosts.

Catching Template Typos

A template property that matches no component member (renamed property, field instead of property) warns instead of silently dropping. Hard failures (unknown component type, null value, cycles) throw with template + member context.

using RoguelikeToolkit.Entities;

EntityDiagnostics.WarningHandler = message => Console.WriteLine($"[template] {message}");

Save / Load

Persist the template name plus dynamic state, then re-spawn and re-apply:

// save
Save("Goblin", entity.Get<Health>().Current, entity.Get<Position>());

// load
factory.TryCreate(savedTemplateName, out var entity);
entity.Set(new Health { Current = savedHp, Max = savedMax });

Modding / Hot-Reload

Loading is additive and folder loads are atomic — on any failure an AggregateException (one inner exception per file) is thrown and the repository is left unchanged:

await repository.LoadTemplateFolderAsync("Mods/Extra", cancellationToken);

There is no unload: hot-reload a mod by loading it into a fresh EntityTemplateRepository + EntityFactory pair and dropping the old one.

Template Format

Author in YAML (comments, readability), ship JSON or a packed single file for spawn-time loading — the same loader reads both (.yaml / .json, case-insensitive). Relative $ref / $merge-ref paths resolve against the referencing file's directory.

Unity / IL2CPP

The netstandard2.1 build has no Reflection.Emit on the load/spawn path, so it runs under IL2CPP. CI publishes a NativeAOT smoke test (stricter than IL2CPP: no JIT, no runtime generic instantiation) to keep it that way.

  • Unity 2022.1+: works as is.

  • Stricter AOT (NativeAOT, older IL2CPP): register each component type once at startup so its closed generic code exists: AotComponentRegistry.Register<Health>();

  • Managed code stripping: component types (and the types of their nested struct/class members) are populated through reflection only. Keep them from being stripped with [Preserve] or a link.xml:

    <linker>
      <assembly fullname="MyGame.Components" preserve="all" />
    </linker>
    
  • Scripts: Jint is a pure managed interpreter (no IL emission); arguments are marshalled without dynamic. Script interop reaches your component types by reflection, so they need the same stripping protection as above. Leave ScriptHostOptions.MemoryLimitBytes off on IL2CPP (it polls GC.GetAllocatedBytesForCurrentThread per statement). Jint is pinned to the version RoguelikeToolkit.World.Scripting.Jint uses: a Unity project can only hold one.

Docs

Benchmarks & Stress Tests

dotnet run --project benchmarks/RoguelikeToolkit.Entities.Benchmarks -c Release
dotnet test Library.sln

Benchmarks (BenchmarkDotNet, allocation + time) catch perf/allocation regressions; the test suite includes stress tests (mass spawn, mass load, concurrent reads). See benchmarks/README.md.

Baseline (Apple M2, macOS Tahoe 26.3.1, .NET 10.0.12, BenchmarkDotNet 0.15.8; spawn rows are per 100-entity batch, resolve rows per single cold resolve + 1 spawn):

Benchmark Mean Allocated
Load_200_Templates 11.54 ms 5.02 MB
BulkLoad_Then_QueryByTag 10.10 ms 4.76 MB
FolderLoad_200_Files 9.56 ms 6.63 MB
Resolve_Cold_SingleLevel 2.22 ms 20.28 KB
Resolve_Cold_TwoLevel 3.32 ms 27.70 KB
Spawn_Simple 6.84 ms 1.30 MB
Spawn_WithEmbedded 5.40 ms 1.20 MB
Spawn_ManyComponents 13.00 ms 2.12 MB
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.0.2 81 10/11/2026
5.1.0 82 10/11/2026
5.0.0 96 10/11/2026
4.0.21 353 1/5/2025
4.0.20 314 1/4/2025
4.0.17 346 4/25/2024
4.0.10 518 1/14/2023
4.0.6 609 9/25/2022
4.0.4 640 9/14/2022
4.0.1 647 9/14/2022
3.0.0 686 4/1/2022
2.2.0 607 9/25/2021
2.1.4 677 9/19/2021
2.1.3 631 9/18/2021
2.1.2 564 9/1/2021
2.1.1 574 8/31/2021
2.0.10 529 8/31/2021
2.0.9 571 8/30/2021
2.0.8 554 8/30/2021
2.0.7 573 8/29/2021
Loading failed

Changes since version v4.0.6.0

:boom: Breaking Changes:
- The `RoguelikeToolkit.Scripts` dependency is replaced by `RoguelikeToolkit.Entities.Scripting` (Jint 4.16.2):
 - namespace `RoguelikeToolkit.Scripts` -> `RoguelikeToolkit.Entities.Scripting`;
 - script constructors take only the source (reference types moved to `ScriptHostOptions.ReferenceTypes`);
 - every execution takes a `ScriptHost` (owns the engine; one per thread) as its first argument;
 - extra arguments are `ScriptArgument` / `(string, object?)` tuples instead of `(string, dynamic)`;
 - `EntityComponentScript.TryExecuteOn` takes the entity by `in` instead of `ref`;
 - scripts are stateless and shared between entities spawned from the same template: top-level `var`s no longer
   persist between executions (state belongs in components).

:sparkles: Features:
- `EntityTemplate.Copy()` creates an editable, unshared variant of a template ("different mobs").
- `EntityTemplateRepository.AddTemplate` stores a defensive copy (clone-on-write at the ownership boundary).
- `EntityTemplateRepository.RemoveTemplate` removes a template together with its tag index entries.
- `EntityTemplateRepository.LoadTemplate` overload accepting in-memory yaml/json string content.
- Shared templates (repository store, effective-template cache) stay shared (flyweight) for reads; mutating one warns through `EntityDiagnostics` (silent by default) instead of corrupting state silently.
- Unity IL2CPP / AOT support: no Reflection.Emit anywhere on the load/spawn path. `Fasterflect` and `ObjectTreeWalker` (Sigil IL emission) are no longer dependencies; component members are written through cached plain reflection, components are set through cached closed generic setters, and primitive/enum/dice/script conversions take an AOT-safe path before falling back to `deniszykov.TypeConversion`.
- `AotComponentRegistry.Register<T>()`: optional per-component AOT hint (closed generic setter + trimming annotations) for runtimes that cannot instantiate generics at runtime (NativeAOT, IL2CPP before 2022.1).
- NativeAOT smoke test (`tests/RoguelikeToolkit.Entities.AotSmoke`, CI job) as the IL2CPP proxy: load, inheritance, nested struct/class, value, dice and global components, scripts, mass spawn.
- `ScriptHost` / `ScriptHostOptions`: one reusable Jint engine with statement, recursion, timeout and (opt-in) memory limits; per-execution function scope and binding cleanup.
- Scripts can use `entity.HasComponent/GetComponent/SetComponent/RemoveComponent(Type)` (exposed types via `ScriptHostOptions.ReferenceTypes`); never worked with the old package.

:zap: Performance:
- Spawning is 6-40x faster with 5-8x less allocation (Spawn_Simple 11.5 ms -> 0.87 ms per 100 entities, 1.28 MB -> 163 KB; same machine, BenchmarkDotNet short job) from dropping per-type IL generation and `MethodInfo.Invoke` on the hot path.
- Script members cost nothing per spawn (shared instance; previously a new Jint engine per entity: ~53 µs, ~21 KB). Executing with an argument: 15.8 µs -> 6.2 µs.

:bug: Fixes:
- `EntityExtensions` child iterator no longer leaks pooled traversal state between calls; tagless entities share an immutable empty tag set; parent-link bookkeeping is thread-safe.
- `GlobalComponentInEntitySetter` actually validates the resolved `SetSameAsWorld` method and warns when a later global value is ignored (first write wins).
- Folder loads stay atomic under racing loads (conflicting batch is rolled back, duplicate raises).
- Duplicate embedded template names and duplicate case-only component properties now warn/fail instead of silently dropping data.
- Single `TypeConversionProvider` factory and single null-guard helper remove four duplicated setups and all repeated `#if` blocks.
- `EntityFactory` spawn errors now name the template and component involved.
- YAML loading no longer depends on reflection-only container constructors (stripped by AOT linkers): `Dictionary`/`List` containers are created directly.
- Component registry tolerates runtimes that cannot enumerate assembly references (scans instead of failing).
- `IValueComponent<T>` detection compares the generic definition instead of a name substring.
- Nested template mappings now report their own unmatched keys through `EntityDiagnostics` (previously only top-level keys were checked).