RoguelikeToolkit.Entities.Scripting.Jint
6.0.5
dotnet add package RoguelikeToolkit.Entities.Scripting.Jint --version 6.0.5
NuGet\Install-Package RoguelikeToolkit.Entities.Scripting.Jint -Version 6.0.5
<PackageReference Include="RoguelikeToolkit.Entities.Scripting.Jint" Version="6.0.5" />
<PackageVersion Include="RoguelikeToolkit.Entities.Scripting.Jint" Version="6.0.5" />
<PackageReference Include="RoguelikeToolkit.Entities.Scripting.Jint" />
paket add RoguelikeToolkit.Entities.Scripting.Jint --version 6.0.5
#r "nuget: RoguelikeToolkit.Entities.Scripting.Jint, 6.0.5"
#:package RoguelikeToolkit.Entities.Scripting.Jint@6.0.5
#addin nuget:?package=RoguelikeToolkit.Entities.Scripting.Jint&version=6.0.5
#tool nuget:?package=RoguelikeToolkit.Entities.Scripting.Jint&version=6.0.5
RoguelikeToolkit.Entities
Data-driven entity templates for DefaultEcs, in the spirit of
Ultimate ADOM's moddable ECS:
describe entities in YAML/JSON, inherit and compose them, spawn them into a DefaultEcs.World.
Targets net10.0 and netstandard2.1 (Unity 2021.3+, IL2CPP-safe). MIT.
Install
- .NET:
dotnet add package RoguelikeToolkit.Entities(depends on DefaultEcs and YamlDotNet only) - Scripting (optional):
dotnet add package RoguelikeToolkit.Entities.Scripting.Jint(adds Jint) - Unity: see Unity / IL2CPP.
Features
- YAML and JSON support: Entity templates can be written in human-readable
.yamlor.jsonformats. - 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 (optional package): Components can carry JavaScript (Jint) scripts for data-driven behavior; parsed once, shared, AOT-safe.
- Dice:
Dice-typed component members are parsed from dice notation (2d6+3,4d6k3); built in, no extra dependencies. - Custom value types: register a parser once (
TemplateValueParsers.Register<T>(...)) and template strings convert to your type; numbers convert range-checked and never lose data silently.
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.
Dice
Dice (namespace RoguelikeToolkit.Entities) members are populated from template strings, or parse one directly:
var damage = Dice.Parse("2d6+3");
damage.Roll(); // thread-local random source
damage.Roll(new Random(seed)); // reproducible rolls (replays, tests)
Dice.TryParse(text, out var d); // no exceptions on bad input
| Syntax | Meaning |
|---|---|
NdS, dS |
N dice with S sides (N defaults to 1); each die rolls 1..S inclusive |
d%, Nd% |
percentile dice (S = 100) |
NdSkK, NdSkhK |
roll N, keep the K highest |
NdSklK |
roll N, keep the K lowest |
NkK |
roll-and-keep shorthand: N d10, keep the K highest |
+ - * /, ( ), unary - |
integer arithmetic; / truncates toward zero; overflow throws |
Case-insensitive, whitespace ignored. Bounds: at most 1000 dice per term, 1,000,000 sides. Expressions are compiled once to a flat postfix program; rolling is allocation-free in steady state and instances are safe to share across threads.
Template value conversion
Template scalars convert to component member types in this order: already assignable; a parser registered for the
member type; enums (case-insensitive name or number); primitives, string, decimal (invariant culture);
Guid, TimeSpan, DateTime, DateTimeOffset, Uri, Version from strings. Out-of-range numbers throw
OverflowException, and a fractional number assigned to an integral member throws FormatException (2.0 is fine,
2.5 is not). Anything else throws InvalidCastException naming the type. Register parsers for your own types:
TemplateValueParsers.Register(static s => Color.FromHex(s)); // thread-safe, last registration wins
Dice is registered out of the box. Parsers run on every spawn, so cache inside the parser when the parsed value is
immutable (the dice and script parsers share one instance per distinct source text).
Scripts
Scripting is a separate package, RoguelikeToolkit.Entities.Scripting.Jint, so the core library carries no
JavaScript engine. Register it once at startup:
using RoguelikeToolkit.Entities.Scripting.Jint;
EntityScripting.Register(); // before spawning templates with script members
Script-typed component members (EntityScript, EntityComponentScript, EntityInteractionScript, Script) are
then populated from template strings. Without the registration, spawning such a template throws
InvalidCastException naming the script type.
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/functiondo not leak between scripts,returnexits early); bindings (entity,component,source/target, arguments) are cleared afterwards. - In scripts,
entity.HasComponent/GetComponent/SetComponent/RemoveComponent(Type)take any type exposed viaScriptHostOptions.ReferenceTypes(the same calls exist in C# asEntityComponentExtensions). Value-type components come back as copies: write them back withSetComponent. - Limits: 500 statements and recursion depth 1024 per execution by default;
TimeoutandMemoryLimitBytesare opt-in. ScriptHostis 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 and no dynamic, so it runs under IL2CPP.
CI runs a NativeAOT smoke test (stricter than IL2CPP: no JIT, no runtime generic instantiation, full trimming) on
every PR to keep it that way.
Install
The UPM package com.rogueliketoolkit.entities ships the prebuilt netstandard2.1 DLL; the optional
com.rogueliketoolkit.entities.scripting.jint adds scripting. Their third-party dependencies (DefaultEcs and
YamlDotNet for core, Jint for scripting) come from UnityNuGet as
org.nuget.* packages instead of being bundled: Unity refuses two copies of the same DLL, and Jint/YamlDotNet are
shared with RoguelikeToolkit.World. Add both scoped registries to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "OpenUPM",
"url": "https://package.openupm.com",
"scopes": ["com.rogueliketoolkit"]
},
{
"name": "UnityNuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": ["org.nuget"]
}
],
"dependencies": {
"com.rogueliketoolkit.entities": "6.0.0",
"com.rogueliketoolkit.entities.scripting.jint": "6.0.0"
}
}
Drop the scripting line if you do not use scripts. (openupm add com.rogueliketoolkit.entities does the OpenUPM half.)
Without registries: every GitHub Release also carries RoguelikeToolkit.Entities-<version>-unity-plugins.zip
with the whole DLL closure plus a link.xml. Unzip into Assets/Plugins/RoguelikeToolkit.Entities/, skipping any DLL
your project already has (System.Runtime.CompilerServices.Unsafe often is).
Runtime notes
Unity 2022.1+: works as is.
Older IL2CPP / NativeAOT: register each component type once at startup so its closed generic code exists:
AotComponentRegistry.Register<Health>();Managed code stripping: the package hands UnityLinker its own
link.xml(an editor hook; Unity ignoreslink.xmlfiles inside packages), preserving this library, DefaultEcs and YamlDotNet (the scripting package does the same for itself and Jint). Your component types (and the types of their nested struct/class members) are populated by reflection only, so they need[Preserve]or alink.xmlof your own underAssets/:<linker> <assembly fullname="MyGame.Components" preserve="all" /> </linker>Scripts: Jint is a pure managed interpreter (no IL emission). Script interop reaches your component types by reflection, so they need the same stripping protection. Leave
ScriptHostOptions.MemoryLimitBytesoff on IL2CPP (Jint pollsGC.GetAllocatedBytesForCurrentThreadper statement). Jint is pinned to the versionRoguelikeToolkit.World.Scripting.Jintuses: a Unity project can only hold one.
Docs
- Usage guide — querying, spawning, pooling, save/load, modding, examples.
- Architecture — internal creation and inheritance flow.
- Integration decisions — why each dependency / perf choice stands.
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 |
Releases
Merging a PR into main computes the next version from conventional commits (GitVersion: feat minor, fix/others
patch, !/BREAKING CHANGE major), tags vX.Y.Z, writes the GitHub Release and publishes the NuGet package plus the
UPM tarball and plugins zip (tools/upm/pack.py, template in upm/com.rogueliketoolkit.entities) as release assets,
which OpenUPM picks up.
| Product | Versions 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. |
-
.NETStandard 2.1
- Jint (>= 4.17.0)
- RoguelikeToolkit.Entities (>= 6.0.5)
-
net10.0
- Jint (>= 4.17.0)
- RoguelikeToolkit.Entities (>= 6.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Changes since version Unreleased
:sparkles: New Features:
- [`4336633`](https://github.com/myarichuk/RoguelikeToolkit.Entities/commit/43366335b93b8f62cf97b3d0f1fe24c3a88aa5a9) - Unity package (com.rogueliketoolkit.entities) published as a release asset for OpenUPM *(commit by [@claude](https://github.com/claude))*