├── .github
└── workflows
│ ├── build.yml
│ └── docs.yml
├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── benchmarks
├── b_explosion.nim
├── b_packed1.nim
├── b_packed5.nim
├── b_updates.nim
├── bench.nim
└── config.nims
├── buildall.sh
├── docs
├── google36b6fdbc1a771529.html
└── index.tpl.html
├── necsus.nimble
├── nim.cfg
├── src
├── necsus.nim
└── necsus
│ ├── compiletime
│ ├── archetype.nim
│ ├── archetypeBuilder.nim
│ ├── attachDetachGen.nim
│ ├── bundleGen.nim
│ ├── codeGenInfo.nim
│ ├── common.nim
│ ├── componentDef.nim
│ ├── converters.nim
│ ├── debugGen.nim
│ ├── deleteGen.nim
│ ├── directiveArg.nim
│ ├── directiveSet.nim
│ ├── dualDirective.nim
│ ├── eventGen.nim
│ ├── localGen.nim
│ ├── lookupGen.nim
│ ├── marshalGen.nim
│ ├── monoDirective.nim
│ ├── parse.nim
│ ├── queryGen.nim
│ ├── restoreGen.nim
│ ├── saveGen.nim
│ ├── sendGen.nim
│ ├── sharedGen.nim
│ ├── spawnGen.nim
│ ├── systemGen.nim
│ ├── tickGen.nim
│ ├── tickIdGen.nim
│ ├── timeGen.nim
│ ├── tools.nim
│ ├── tupleDirective.nim
│ └── worldGen.nim
│ ├── runtime
│ ├── archetypeStore.nim
│ ├── directives.nim
│ ├── entityId.nim
│ ├── inbox.nim
│ ├── necsusConf.nim
│ ├── pragmas.nim
│ ├── query.nim
│ ├── spawn.nim
│ ├── systemVar.nim
│ ├── tuples.nim
│ └── world.nim
│ └── util
│ ├── bits.nim
│ ├── blockstore.nim
│ ├── dump.nim
│ ├── nimNode.nim
│ ├── profile.nim
│ ├── tools.nim
│ └── typeReader.nim
└── tests
├── bundle_include.nim
├── config.nims
├── privateSystem.nim
├── t_accessory.nim
├── t_accessory_attach.nim
├── t_accessory_detach.nim
├── t_accessory_len.nim
├── t_accessory_lookup.nim
├── t_accessory_not.nim
├── t_accessory_optional.nim
├── t_accessory_optional_detach.nim
├── t_accessory_optionptr.nim
├── t_accessory_pragma.nim
├── t_accessory_swap.nim
├── t_aliasWithCompGeneric.nim
├── t_aliasWithGenerics.nim
├── t_aliases.nim
├── t_appReturn.nim
├── t_archetypeBuilder.nim
├── t_attach.nim
├── t_attachFiltering.nim
├── t_attach_disjoint.nim
├── t_basic.nim
├── t_bits.nim
├── t_blockstore.nim
├── t_bundle.nim
├── t_bundleEmptyObject.nim
├── t_bundleFromBuiltSystem.nim
├── t_bundleInstanced.nim
├── t_bundleNested.nim
├── t_bundleWithGenerics.nim
├── t_bundleWithInbox.nim
├── t_bundleWithLocal.nim
├── t_bundleWithTimes.nim
├── t_componentTuple.nim
├── t_defaultRunner.nim
├── t_delete.nim
├── t_deleteAll.nim
├── t_deleteCallsDestroy.nim
├── t_depends.nim
├── t_dependsOnPrivate.nim
├── t_detach.nim
├── t_detachOptional.nim
├── t_detachRequiresAll.nim
├── t_detachWithoutCopy.nim
├── t_directiveAlias.nim
├── t_entityDebug.nim
├── t_eventDistinction.nim
├── t_eventRefs.nim
├── t_eventSys.nim
├── t_eventSysCall.nim
├── t_eventSysCycle.nim
├── t_eventSysInstanced.nim
├── t_eventUnion.nim
├── t_events.nim
├── t_eventsMisordered.nim
├── t_eventsOnDisabledSystems.nim
├── t_eventsWithoutInbox.nim
├── t_eventsWithoutOutbox.nim
├── t_events_varSystem.nim
├── t_genericComponents.nim
├── t_instancedObj.nim
├── t_instancedProc.nim
├── t_instancedSharedVar.nim
├── t_largeEntitySets.nim
├── t_local.nim
├── t_lookup.nim
├── t_lookupNot.nim
├── t_lookupPtr.nim
├── t_lookupWithoutArchetypes.nim
├── t_manual.nim
├── t_maxCapacity.nim
├── t_missingEntities.nim
├── t_necsusPragmas.nim
├── t_noComponents.nim
├── t_openSym.nim
├── t_optionalQuery.nim
├── t_outsideEvents.nim
├── t_outsideEventsFromEventSys.nim
├── t_passSpawn.nim
├── t_phases.nim
├── t_queryLen.nim
├── t_queryNot.nim
├── t_queryOne.nim
├── t_queryUpdates.nim
├── t_queryWithPointers.nim
├── t_queryWithoutSpawns.nim
├── t_recycle.nim
├── t_restore.nim
├── t_restoreMissingKey.nim
├── t_restoreWithoutSave.nim
├── t_runSystemOnce.nim
├── t_runSystemOnceMultipleDefs.nim
├── t_runnerArgs.nim
├── t_save.nim
├── t_saveInstanced.nim
├── t_sharedVar.nim
├── t_sharedVarDefaults.nim
├── t_sharedVarModify.nim
├── t_sharedVarVariousTypes.nim
├── t_sizeFromVar.nim
├── t_spawnExtending.nim
├── t_spawnWithoutCopy.nim
├── t_spawn_duplicated.nim
├── t_stateFromVar.nim
├── t_stateUnused.nim
├── t_swap.nim
├── t_swapOptional.nim
├── t_swapRequiresAll.nim
├── t_sysPragmas.nim
├── t_systemState.nim
├── t_tickId.nim
├── t_tickIdStorage.nim
├── t_timeDelta.nim
├── t_timeElapsed.nim
├── t_tuples.nim
├── t_update.nim
└── t_variableSystem.nim
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push, pull_request]
3 | jobs:
4 |
5 | test:
6 | runs-on: ubuntu-latest
7 | container: nimlang/choosenim
8 | strategy:
9 | matrix:
10 | nim: [ 2.0.10, 1.6.14 ]
11 | steps:
12 | - uses: actions/checkout@v1
13 | - name: Choose Nim
14 | run: choosenim update -y ${{ matrix.nim }}
15 | - name: Safe git directory
16 | run: git config --global --add safe.directory "$(pwd)"
17 | - name: Test
18 | run: nimble test -y
19 |
20 | benchmark:
21 | runs-on: ubuntu-latest
22 | container: nimlang/choosenim
23 | strategy:
24 | matrix:
25 | nim: [ 2.0.10, 1.6.14 ]
26 | steps:
27 | - uses: actions/checkout@v1
28 | - name: Choose Nim
29 | run: choosenim update -y ${{ matrix.nim }}
30 | - name: Safe git directory
31 | run: git config --global --add safe.directory "$(pwd)"
32 | - name: Benchmark
33 | run: nimble -y -d:release benchmark
34 |
35 | readme:
36 | runs-on: ubuntu-latest
37 | container: nimlang/choosenim
38 | strategy:
39 | matrix:
40 | nim: [ 2.0.10, 1.6.14 ]
41 | steps:
42 | - uses: actions/checkout@v1
43 | - name: Choose Nim
44 | run: choosenim update -y ${{ matrix.nim }}
45 | - name: Safe git directory
46 | run: git config --global --add safe.directory "$(pwd)"
47 | - name: Build readme code
48 | run: nimble readme
49 |
50 | example-projects:
51 | runs-on: ubuntu-latest
52 | container: nimlang/choosenim
53 | strategy:
54 | matrix:
55 | project: [NecsusECS/NecsusAsteroids, NecsusECS/NecsusParticleDemo]
56 | nim: [ 2.0.10, 1.6.14 ]
57 | steps:
58 | - uses: actions/checkout@v1
59 | - name: Choose Nim
60 | run: choosenim update -y ${{ matrix.nim }}
61 | - name: Safe git directory
62 | run: git config --global --add safe.directory "$(pwd)"
63 | - name: Local override
64 | run: nimble develop
65 | - name: Checkout
66 | run: git clone https://github.com/${{ matrix.project }}.git project
67 | - name: Build
68 | run: cd project && nimble build -y
69 |
70 | flags:
71 | ## Confirm the tests are able to run in profiling mode
72 | runs-on: ubuntu-latest
73 | container: nimlang/choosenim
74 | strategy:
75 | matrix:
76 | nim: [ 2.0.10 ]
77 | flag: [
78 | profile,
79 | dump,
80 | archetypes,
81 | necsusSystemTrace,
82 | necsusEntityTrace,
83 | necsusEventTrace,
84 | necsusQueryTrace,
85 | necsusSaveTrace
86 | ]
87 | steps:
88 | - uses: actions/checkout@v1
89 | - name: Choose Nim
90 | run: choosenim update -y ${{ matrix.nim }}
91 | - name: Safe git directory
92 | run: git config --global --add safe.directory "$(pwd)"
93 | - name: Test
94 | run: nimble -d:${{ matrix.flag }} test
95 |
96 | fast-compile:
97 | ## Confirm all the tests compile when running in a 'fast compile' mode
98 | runs-on: ubuntu-latest
99 | container: nimlang/choosenim
100 | strategy:
101 | matrix:
102 | nim: [ 2.0.10, 1.6.14 ]
103 | steps:
104 | - uses: actions/checkout@v1
105 | - name: Choose Nim
106 | run: choosenim update -y ${{ matrix.nim }}
107 | - name: Safe git directory
108 | run: git config --global --add safe.directory "$(pwd)"
109 | - name: Nim suggest
110 | run: find tests -name "t_*.nim" | xargs -n1 sh -c 'nim c -d:nimsuggest $0 || exit 255'
111 | - name: Nim check
112 | run: find tests -name "t_*.nim" | xargs -n1 sh -c 'nim check $0 || exit 255'
113 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Documentation Deploy
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | docs:
8 | runs-on: ubuntu-latest
9 | container: nimlang/choosenim
10 | steps:
11 | - name: Choose Nim
12 | run: choosenim update -y 2.0.10
13 | - uses: actions/checkout@v3
14 | - run: git config --global --add safe.directory "$(pwd)"
15 | - run: nimble install -y markdown
16 | - run: nimble documentation
17 | - name: Deploy documents
18 | uses: peaceiris/actions-gh-pages@v3
19 | if: ${{ !env.ACT }}
20 | with:
21 | github_token: ${{ secrets.GITHUB_TOKEN }}
22 | publish_dir: docs
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore all
2 | tests/t_*
3 | benchmarks/*
4 |
5 | # Unignore all with extensions
6 | !tests/t_*.*
7 | !benchmarks/*.*
8 |
9 | *.js
10 |
11 | .DS_Store
12 | callgrind.out.*
13 | profile_results.txt
14 | docs
15 | *.idx
16 | *.css
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "type": "lldb",
5 | "request": "launch",
6 | "name": "Debug",
7 | "program": "${fileDirname}/${fileBasenameNoExtension}",
8 | "preLaunchTask": "Compile Test",
9 | "presentation": {
10 | "hidden": false
11 | }
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Compile Test",
8 | "type": "shell",
9 | "command": "nimble c --debugger:native ${file}",
10 | "group": {
11 | "kind": "build",
12 | "isDefault": true
13 | },
14 | "problemMatcher": [],
15 | "presentation": {
16 | "echo": true,
17 | "reveal": "always",
18 | "focus": false,
19 | "panel": "shared",
20 | "showReuseMessage": false,
21 | "clear": true,
22 | "close": false
23 | }
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/benchmarks/b_explosion.nim:
--------------------------------------------------------------------------------
1 | import necsus, bench
2 |
3 | type
4 | A = distinct int
5 | B = distinct int
6 | C = distinct int
7 | D = distinct int
8 | E = distinct int
9 | F = distinct int
10 | G = distinct int
11 | H = distinct int
12 | I = distinct int
13 | J = distinct int
14 | K = distinct int
15 | L = distinct int
16 | M = distinct int
17 |
18 | proc setup(
19 | spawn1: FullSpawn[(A, B, C)],
20 | spawn2: FullSpawn[(C, D, E)],
21 | spawn3: FullSpawn[(E, F, G)],
22 | spawn4: FullSpawn[(G, H, I)],
23 | attach1: Attach[(J, )],
24 | attach2: Attach[(K, )],
25 | attach3: Attach[(L, )],
26 | attach4: Attach[(M, )],
27 | ) {.startupSys.} =
28 | for i in 1..100:
29 | spawn1.with(A(i), B(i), C(i)).attach1((J(i), ))
30 | spawn2.with(C(i), D(i), E(i)).attach2((K(i), ))
31 | spawn3.with(E(i), F(i), G(i)).attach3((L(i), ))
32 | spawn4.with(G(i), H(i), I(i)).attach4((M(i), ))
33 |
34 | var storage: int
35 |
36 | proc query(
37 | j: FullQuery[(J, )],
38 | k: FullQuery[(K, )],
39 | l: FullQuery[(L, )],
40 | m: FullQuery[(M, )],
41 | ) =
42 | for entity, comp in j:
43 | storage = int(comp[0])
44 | for entity, comp in k:
45 | storage = int(comp[0])
46 | for entity, comp in l:
47 | storage = int(comp[0])
48 | for entity, comp in m:
49 | storage = int(comp[0])
50 |
51 | proc runner(tick: proc(): void) =
52 | tick()
53 | benchmark "Archetype explosion", 1000:
54 | tick()
55 |
56 | proc myApp() {.necsus(runner, [~setup, ~query], newNecsusConf(10_000, eagerAlloc = true)).}
57 |
58 | myApp()
59 |
60 |
--------------------------------------------------------------------------------
/benchmarks/b_packed1.nim:
--------------------------------------------------------------------------------
1 | import necsus, bench
2 |
3 | type
4 | A = distinct int
5 | B = distinct int
6 | C = distinct int
7 | D = distinct int
8 | E = distinct int
9 |
10 | proc setup(spawn: Spawn[(A, B, C, D, E)]) {.startupSys.} =
11 | for i in 1..1000:
12 | spawn.with(A(i), B(i), C(i), D(i), E(i))
13 |
14 | proc modify(a: FullQuery[(A, )], attach: Attach[(A, )]) =
15 | for entity, comp in a:
16 | attach(entity, (A(int(comp[0]) * 2), ))
17 |
18 | proc runner(tick: proc(): void) =
19 | tick()
20 | benchmark "Packed iteration with 1 query and 1 system: https://github.com/noctjs/ecs-benchmark/", 1000:
21 | tick()
22 |
23 | proc myApp() {.necsus(runner, [~setup, ~modify], newNecsusConf(10_000, eagerAlloc = true)).}
24 |
25 | myApp()
26 |
--------------------------------------------------------------------------------
/benchmarks/b_packed5.nim:
--------------------------------------------------------------------------------
1 | import necsus, bench
2 |
3 | type
4 | A = distinct int
5 | B = distinct int
6 | C = distinct int
7 | D = distinct int
8 | E = distinct int
9 |
10 | proc setup(spawn: Spawn[(A, B, C, D, E)]) =
11 | for i in 1..1000:
12 | spawn.with(A(i), B(i), C(i), D(i), E(i))
13 |
14 | template setupSystem(typ: typedesc) =
15 | proc `modify typ`(query: FullQuery[(typ, )], attach: Attach[(typ, )]) =
16 | for entity, comp in query:
17 | attach(entity, (typ(int(comp[0]) * 2), ))
18 |
19 | setupSystem(A)
20 | setupSystem(B)
21 | setupSystem(C)
22 | setupSystem(D)
23 | setupSystem(E)
24 |
25 | proc runner(tick: proc(): void) =
26 | tick()
27 | benchmark "Packed iteration with 1 query and 5 systems: https://github.com/noctjs/ecs-benchmark/", 5000:
28 | tick()
29 |
30 | proc myApp() {.necsus(
31 | runner,
32 | [~setup, ~modifyA, ~modifyB, ~modifyC, ~modifyD, ~modifyE],
33 | newNecsusConf(10_000, eagerAlloc = true)
34 | ).}
35 |
36 | myApp()
37 |
--------------------------------------------------------------------------------
/benchmarks/b_updates.nim:
--------------------------------------------------------------------------------
1 | import necsus, bench, times
2 |
3 | type
4 | Position {.byref.} = object
5 | x: float
6 | y: float
7 |
8 | Direction {.byref.} = object
9 | x: float
10 | y: float
11 |
12 | Comflabulation {.byref.} = object
13 | thingy: float
14 | dingy: int
15 | mingy: bool
16 | stringy: string
17 |
18 | const entityCount = 1_000_000
19 |
20 | proc setup(spawn: Spawn[(Comflabulation, Direction, Position)]) {.startupSys.} =
21 | spawn.with(Comflabulation(), Direction(), Position())
22 | benchmark "Creating " & $entityCount & " entities", entityCount:
23 | for i in 1..entityCount:
24 | spawn.with(Comflabulation(), Direction(), Position())
25 |
26 | proc movement(dt: TimeDelta, entities: Query[tuple[pos: ptr Position, dir: Direction]]) =
27 | for comp in entities:
28 | comp.pos.x = comp.pos.x + (comp.dir.x * dt())
29 | comp.pos.y = comp.pos.y + (comp.dir.y * dt())
30 |
31 | proc comflab(entities: Query[tuple[comflab: ptr Comflabulation]]) =
32 | for comp in entities:
33 | comp.comflab.thingy = comp.comflab.thingy * 1.000001f
34 | comp.comflab.mingy = not comp.comflab.mingy
35 | comp.comflab.dingy = comp.comflab.dingy + 1
36 |
37 | proc runner(tick: proc(): void) =
38 | tick()
39 | benchmark "Updating " & $entityCount & " components: https://github.com/abeimler/ecs_benchmark", entityCount:
40 | tick()
41 |
42 | proc myApp() {.necsus(
43 | runner,
44 | [~setup, ~movement, ~comflab],
45 | newNecsusConf(entityCount * 2, entityCount * 2, eagerAlloc = true)
46 | ).}
47 |
48 | myApp()
49 |
--------------------------------------------------------------------------------
/benchmarks/bench.nim:
--------------------------------------------------------------------------------
1 | import times, os, strutils
2 |
3 | template benchmark*(benchmarkName: string, totalOps: int, code: untyped) =
4 | block:
5 | let t0 = epochTime()
6 | code
7 | let elapsed = epochTime() - t0
8 | echo benchmarkName
9 | echo " CPU Time: ", formatFloat(elapsed * 1_000, ffDecimal, precision = 4), " ms"
10 | echo " Ops per second: ", formatFloat(float(totalOps) / elapsed, ffDecimal, precision = 2), " op/s"
11 | echo " Op speed: ", formatFloat(elapsed / float(totalOps) * 1_000_000_000, ffDecimal, precision = 2), " ns/op"
12 |
--------------------------------------------------------------------------------
/benchmarks/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../src")
2 | --d:release
3 | --threads:on
4 | --debugger:native
5 | --verbosity:0
6 | --hints:off
7 | --boundChecks:off
8 | --assertions:off
9 |
--------------------------------------------------------------------------------
/buildall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -xeuf -o pipefail
4 |
5 | for nimVersion in 2.0.0 1.6.14; do
6 | for threads in on off; do
7 | for target in benchmark test; do
8 | act -W .github/workflows/build.yml -j "$target" --matrix "nim:$nimVersion" --matrix "threads:$threads";
9 | done
10 | done
11 |
12 | for project in NecsusECS/NecsusAsteroids NecsusECS/NecsusParticleDemo; do
13 | act -W .github/workflows/build.yml -j example-projects --matrix "nim:$nimVersion" --matrix "project:$project";
14 | done
15 |
16 | act -W .github/workflows/build.yml -j readme --matrix "nim:$nimVersion";
17 | done
18 |
19 | for flag in profile dump archetypes necsusFloat32; do
20 | act -W .github/workflows/build.yml -j flags --matrix "nim:$nimVersion" --matrix "flag:$flag";
21 | done
22 |
23 | act -W .github/workflows/docs.yml
--------------------------------------------------------------------------------
/docs/google36b6fdbc1a771529.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google36b6fdbc1a771529.html
--------------------------------------------------------------------------------
/docs/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Necsus: An ECS (Entity Component System) for Nim
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 | {body}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/necsus.nimble:
--------------------------------------------------------------------------------
1 | # Package
2 |
3 | version = "0.12.0"
4 | author = "Nycto"
5 | description = "Entity Component System"
6 | license = "MIT"
7 | srcDir = "src"
8 |
9 | # Dependencies
10 |
11 | requires "nim >= 1.6.0"
12 |
13 | import os
14 |
15 | task benchmark, "Executes a suite of benchmarks":
16 | for file in listFiles("benchmarks"):
17 | if file.startsWith("benchmarks/b_") and file.endsWith(".nim"):
18 | echo "Executing: ", file
19 | exec("nim r " & file)
20 |
21 | task readme, "Compiles code in the readme":
22 | let readme = readFile("README.md")
23 | var inCode = false
24 | var accum = ""
25 | var count = 0
26 | for line in readme.split("\n"):
27 | if line.startsWith "```":
28 |
29 | if inCode:
30 | let tmpPath = getTempDir() & "necsus_readme_" & $count & ".nim"
31 | writeFile(tmpPath, accum)
32 | exec("nim c -r -p:" & getCurrentDir() & "/src --experimental:callOperator --threads:on " & tmpPath)
33 | accum = ""
34 | count += 1
35 |
36 | inCode = not inCode
37 | elif inCode:
38 | accum &= line & "\n"
39 |
40 | task documentation, "Generates API documentation":
41 | exec("nimble -y doc --index:on --out:docs --project src/necsus.nim")
42 |
43 | let (body, code) = gorgeEx("~/.nimble/bin/markdown < README.md")
44 | assert(code == 0, body)
45 | writeFile("docs/index.html", readFile("docs/index.tpl.html").replace("{body}", body))
46 |
--------------------------------------------------------------------------------
/nim.cfg:
--------------------------------------------------------------------------------
1 | --gc:orc
2 | --threads:on
3 | --warning[ProveInit]:off
4 | --experimental:callOperator
5 |
--------------------------------------------------------------------------------
/src/necsus.nim:
--------------------------------------------------------------------------------
1 | ##
2 | ## Necsus: An ECS (entity component system) for Nim
3 | ##
4 | ## In depth documentation can be found here:
5 | ##
6 | ## * https://necsusecs.github.io/Necsus/
7 | ##
8 |
9 | import
10 | necsus/runtime/
11 | [entityId, query, systemVar, inbox, directives, necsusConf, spawn, pragmas, tuples]
12 | import necsus/compiletime/[parse, systemGen, codeGenInfo, worldGen, archetype]
13 | import necsus/compiletime/[tickGen, common, marshalGen, sendGen]
14 | import necsus/util/dump
15 | import sequtils, macros, options
16 |
17 | export
18 | entityId, query, query.items, necsusConf, systemVar, inbox, directives, spawn,
19 | pragmas, tuples
20 |
21 | type
22 | SystemFlag* = object
23 | ## Fixes type checking errors when passing system procs into the necsus macro
24 |
25 | NecsusRun* = enum
26 | ## For the default game loop runner, tells the loop when to exit
27 | RunLoop
28 | ExitLoop
29 |
30 | proc `~`*(system: proc): SystemFlag =
31 | ## Ensures that system macros with various arguments are able to be massed in to the necsus macro
32 | SystemFlag()
33 |
34 | proc gameLoop*(exit: Shared[NecsusRun], tick: proc(): void) =
35 | ## A standard game loop runner
36 | while exit.get(RunLoop) == RunLoop:
37 | tick()
38 |
39 | proc buildApp(
40 | runner: NimNode, systems: NimNode, conf: NimNode, pragmaProc: NimNode
41 | ): NimNode =
42 | ## Creates an ECS world
43 |
44 | let parsedApp = parseApp(pragmaProc, runner)
45 | let parsedSystems = parseSystemList(systems)
46 |
47 | let codeGenInfo = newCodeGenInfo(conf, parsedApp, parsedSystems)
48 |
49 | result = newStmtList(
50 | codeGenInfo.createArchetypeIdSyms(),
51 | codeGenInfo.createAppStateType(),
52 | codeGenInfo.createAppStateDestructor(),
53 | codeGenInfo.createConverterProcs(),
54 | codeGenInfo.createMarshalProcs(),
55 | codeGenInfo.createSendProcs(),
56 | codeGenInfo.generateForHook(GenerateHook.Outside),
57 | codeGenInfo.createAppStateInit(),
58 | codeGenInfo.createTickProc(),
59 | pragmaProc,
60 | )
61 |
62 | pragmaProc.body = newStmtList(
63 | codeGenInfo.createAppStateInstance(),
64 | codeGenInfo.createTickRunner(runner),
65 | codeGenInfo.createAppReturn(pragmaProc),
66 | )
67 |
68 | if defined(archetypes):
69 | codeGenInfo.archetypes.dumpAnalysis
70 |
71 | if defined(dump):
72 | result.dumpGeneratedCode(parsedApp, parsedSystems)
73 |
74 | macro necsus*(
75 | runner: typed{sym},
76 | systems: openarray[SystemFlag],
77 | conf: NecsusConf,
78 | pragmaProc: untyped,
79 | ) =
80 | ## Creates an ECS world
81 | buildApp(runner, systems, conf, pragmaProc)
82 |
83 | macro necsus*(systems: openarray[SystemFlag], conf: NecsusConf, pragmaProc: untyped) =
84 | ## Creates an ECS world
85 | buildApp(bindSym("gameLoop"), systems, conf, pragmaProc)
86 |
87 | macro runSystemOnce*(systemDef: typed): untyped =
88 | ## Creates a single system and immediately executes it with a specific set of directives
89 |
90 | let systemIdent = genSym()
91 | let system = parseSystemDef(systemIdent, systemDef)
92 |
93 | let necsusConfIdent = genSym()
94 | let defineConf = quote:
95 | let `necsusConfIdent` = newNecsusConf()
96 |
97 | let app = newEmptyApp(
98 | "App_" & $lineInfoObj(systemDef).line & "_" & $lineInfoObj(systemDef).column
99 | )
100 | let codeGenInfo = newCodeGenInfo(necsusConfIdent, app, @[system])
101 | let initIdent = codeGenInfo.appStateInit
102 |
103 | let call = newCall(systemIdent, system.args.mapIt(systemArg(codeGenInfo, it)))
104 |
105 | let appStateType = codeGenInfo.appStateTypeName
106 |
107 | result = newStmtList(
108 | codeGenInfo.createArchetypeIdSyms(),
109 | codeGenInfo.createAppStateType(),
110 | codeGenInfo.createAppStateDestructor(),
111 | codeGenInfo.createConverterProcs(),
112 | codeGenInfo.createMarshalProcs(),
113 | codeGenInfo.createSendProcs(),
114 | codeGenInfo.generateForHook(GenerateHook.Outside),
115 | defineConf,
116 | codeGenInfo.createAppStateInit(),
117 | quote do:
118 | block:
119 | var `appStateIdent`: `appStateType`
120 | `initIdent`(`appStateIdent`)
121 | let `systemIdent` = `systemDef`
122 | `call`,
123 | )
124 |
125 | if defined(dump):
126 | result.dumpGeneratedCode(app, @[system])
127 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/bundleGen.nim:
--------------------------------------------------------------------------------
1 | import
2 | macros,
3 | monoDirective,
4 | systemGen,
5 | std/[importutils, options],
6 | common,
7 | ../util/typeReader
8 |
9 | proc worldFields(name: string, dir: MonoDirective): seq[WorldField] =
10 | @[(name, dir.argType)]
11 |
12 | proc generate(
13 | details: GenerateContext, arg: SystemArg, name: string, dir: MonoDirective
14 | ): NimNode =
15 | result = newStmtList()
16 | let nameIdent = ident(name)
17 |
18 | case details.hook
19 | of Late:
20 | let bundleType = dir.argType
21 | let construct = nnkObjConstr.newTree(bundleType)
22 |
23 | for nested in arg.nestedArgs:
24 | construct.add(
25 | nnkExprColonExpr.newTree(ident(nested.originalName), details.systemArg(nested))
26 | )
27 |
28 | result.add quote do:
29 | privateAccess(`appStateIdent`.`nameIdent`.type)
30 | `appStateIdent`.`nameIdent` = `construct`
31 | else:
32 | discard
33 |
34 | proc systemArg(name: string, dir: MonoDirective): NimNode =
35 | let nameIdent = name.ident
36 | return quote:
37 | addr `appStateIdent`.`nameIdent`
38 |
39 | proc nestedArgs(dir: MonoDirective): seq[RawNestedArg] =
40 | ## Looks up all the fields on the bundled object and returns them as nested fields
41 |
42 | let bundleImpl = dir.argType.resolveTo({nnkObjectTy})
43 |
44 | let impl: NimNode =
45 | if bundleImpl.isSome:
46 | bundleImpl.get
47 | else:
48 | dir.argType.expectKind(nnkObjectTy)
49 | newEmptyNode()
50 |
51 | impl[2].expectKind({nnkRecList, nnkEmpty})
52 |
53 | for child in impl[2].children:
54 | child.expectKind(nnkIdentDefs)
55 |
56 | let name =
57 | if child[0].kind == nnkPostfix:
58 | child[0][1]
59 | else:
60 | child[0]
61 | name.expectKind(nnkIdent)
62 | result.add((dir.argType, name, child[1]))
63 |
64 | let bundleGenerator* {.compileTime.} = newGenerator(
65 | ident = "Bundle",
66 | interest = {Late},
67 | generate = generate,
68 | worldFields = worldFields,
69 | systemArg = systemArg,
70 | nestedArgs = nestedArgs,
71 | )
72 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/common.nim:
--------------------------------------------------------------------------------
1 | import std/macros
2 |
3 | ## The variable used to reference the initial size of any structs
4 | let confIdent* {.compileTime.} = ident("config")
5 |
6 | ## The variable holding the app state instance
7 | let appStateIdent* {.compileTime.} = ident("appState")
8 |
9 | ## The variable holding a pointer to the app state
10 | let appStatePtr* {.compileTime.} = ident("appStatePtr")
11 |
12 | ## Property that stores the current lifecycle of the app
13 | let lifecycle* {.compileTime.} = ident("lifecycle")
14 |
15 | ## The variable for identifying the local world
16 | let worldIdent* {.compileTime.} = ident("world")
17 |
18 | ## A variable that represents the time that the current tick started
19 | let thisTime* {.compileTime.} = ident("thisTime")
20 |
21 | ## A variable that represents the time that execution started
22 | let startTime* {.compileTime.} = ident("startTime")
23 |
24 | template isFastCompileMode*(title: untyped): bool =
25 | ## Returns whether the compiler should elide complicated function content
26 | ## that tends to slow down compilation. This is useful, for example, to speed
27 | ## up IDE integration
28 | (defined(nimsuggest) or defined(nimcheck)) and not defined(title)
29 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/componentDef.nim:
--------------------------------------------------------------------------------
1 | import std/[macros, hashes, sequtils, strutils, macrocache, options, strformat]
2 | import ../util/[nimNode, typeReader], ../runtime/pragmas
3 |
4 | type ComponentDef* = ref object ## An individual component symbol within the ECS
5 | node*: NimNode
6 | name*: string
7 | uniqueId*: uint16
8 | isAccessory*: bool
9 |
10 | const ids = CacheCounter("NecsusComponentIds")
11 |
12 | when NimMajor >= 2:
13 | const lookup = CacheTable("NecsusComponentIdCache")
14 | else:
15 | import std/tables
16 | var lookup {.compileTime.} = initTable[string, NimNode]()
17 |
18 | proc getArchetypeValueId(value: NimNode): uint16 =
19 | var sig: string
20 | sig.addSignature(value)
21 |
22 | if sig notin lookup:
23 | lookup[sig] = ids.value.newLit
24 | ids.inc
25 |
26 | return lookup[sig].intVal.uint16
27 |
28 | proc newComponentDef*(node: NimNode): ComponentDef =
29 | ## Instantiate a ComponentDef
30 | let id = getArchetypeValueId(node)
31 | ComponentDef(
32 | node: node,
33 | name: "c" & $id,
34 | uniqueId: id,
35 | isAccessory: node.hasPragma(bindSym("accessory")),
36 | )
37 |
38 | proc readableName*(comp: ComponentDef): string =
39 | ## Returns a human readable name for a node
40 | comp.node.symbols.join("_")
41 |
42 | proc `==`*(a, b: ComponentDef): bool =
43 | ## Compare two ComponentDef instances
44 | a.uniqueId == b.uniqueId
45 |
46 | proc `<`*(a, b: ComponentDef): auto =
47 | cmp(a.node, b.node) < 0
48 |
49 | proc `$`*(def: ComponentDef): string =
50 | ## Stringify a ComponentDef
51 | $(def.node.repr)
52 |
53 | proc generateName*(components: openarray[ComponentDef]): string =
54 | ## Creates a name to describe the given components
55 | components.mapIt(it.name).join("_")
56 |
57 | proc ident*(def: ComponentDef): NimNode =
58 | ## Stringify a ComponentDef
59 | result = copy(def.node)
60 | result.copyLineInfo(def.node)
61 |
62 | proc hash*(def: ComponentDef): Hash =
63 | def.uniqueId.hash
64 |
65 | proc addSignature*(onto: var string, comp: ComponentDef) =
66 | ## Generate a unique ID for a component
67 | onto &= comp.name
68 |
69 | when NimMajor >= 2:
70 | const capacityCache = CacheTable("NecsusCapacityCache")
71 | else:
72 | var capacityCache {.compileTime.} = initTable[string, NimNode]()
73 |
74 | proc getCapacity(node: NimNode): Option[NimNode] =
75 | case node.kind
76 | of nnkSym:
77 | let hash = node.signatureHash
78 | if hash in capacityCache:
79 | let cached = capacityCache[hash]
80 | return
81 | if cached.kind == nnkEmpty:
82 | none(NimNode)
83 | else:
84 | some(cached)
85 |
86 | var res = node.getImpl.getCapacity()
87 | if res.isNone:
88 | let dealiased = node.resolveAlias()
89 | if dealiased.isSome:
90 | res = dealiased.get.getCapacity()
91 |
92 | capacityCache[hash] =
93 | if res.isSome:
94 | res.get
95 | else:
96 | newEmptyNode()
97 |
98 | return res
99 | of nnkObjectTy, nnkTypeDef:
100 | for pragma in node.findPragma:
101 | if pragma.isPragma(bindSym("maxCapacity")):
102 | return some(pragma[1])
103 | of nnkBracketExpr:
104 | return node[0].getCapacity
105 | else:
106 | return none(NimNode)
107 |
108 | proc maxCapacity*(errorSite: NimNode, components: auto): Option[NimNode] =
109 | ## Calculates the storage size required to store a list of components
110 | for comp in components:
111 | assert(comp is ComponentDef)
112 | let capacity = comp.node.getCapacity
113 | if capacity.isSome:
114 | let newValue = newCall("uint", capacity.get)
115 | if result.isSome:
116 | result = some(newCall(bindSym("max"), result.get, newValue))
117 | else:
118 | result = some(newValue)
119 |
120 | when defined(requireMaxCapacity):
121 | if result.isNone:
122 | for comp in components:
123 | hint(fmt"{comp} does not have a maxCapacity pragma", comp.node)
124 | error(
125 | fmt"Must have at least one component with a maxCapacity defined: {components}",
126 | errorSite,
127 | )
128 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/debugGen.nim:
--------------------------------------------------------------------------------
1 | import macros, options, tables
2 | import tools, common, archetype, componentDef, systemGen
3 | import ../runtime/[world, archetypeStore, directives], ../util/tools
4 |
5 | let entityId {.compileTime.} = ident("entityId")
6 |
7 | let entityIndex {.compileTime.} = ident("entityIndex")
8 |
9 | let compsIdent {.compileTime.} = ident("comps")
10 |
11 | let entityArchetype {.compileTime.} = newDotExpr(entityIndex, ident("archetype"))
12 |
13 | proc worldFields(name: string): seq[WorldField] =
14 | @[(name, bindSym("EntityDebug"))]
15 |
16 | proc buildArchetypeLookup(
17 | details: GenerateContext, archetype: Archetype[ComponentDef]
18 | ): NimNode =
19 | ## Builds the block of code for pulling a lookup out of a specific archetype
20 |
21 | let archetypeType = archetype.asStorageTuple
22 | let archetypeIdent = archetype.ident
23 |
24 | let archetypeIdentVar =
25 | newLit(" = " & archetype.readableName & " (" & archetype.idSymbol.strVal & ")")
26 |
27 | var str = quote:
28 | $`entityId` & `archetypeIdentVar`
29 |
30 | var i = 0
31 | for comp in archetype:
32 | let label = newLit("; " & comp.readableName & " = ")
33 | str = quote:
34 | `str` & `label` & stringify(`compsIdent`[`i`])
35 | i += 1
36 |
37 | return quote:
38 | let `compsIdent` = getComps[`archetypeType`](
39 | `appStateIdent`.`archetypeIdent`, `entityIndex`.archetypeIndex
40 | )
41 | return `str`
42 |
43 | proc generateEntityDebug(
44 | details: GenerateContext, arg: SystemArg, name: string
45 | ): NimNode =
46 | ## Generates the code for debugging the state of an entity
47 | if isFastCompileMode(fastDebugGen):
48 | return newEmptyNode()
49 |
50 | let debugProc = details.globalName(name)
51 |
52 | case details.hook
53 | of GenerateHook.Outside:
54 | let appType = details.appStateTypeName
55 |
56 | # Create a case statement where each branch is one of the archetypes
57 | var cases = newEmptyNode()
58 |
59 | when not defined(release):
60 | if details.archetypes.len > 0:
61 | cases = nnkCaseStmt.newTree(entityArchetype)
62 | for (ofBranch, archetype) in archetypeCases(details):
63 | cases.add(
64 | nnkOfBranch.newTree(ofBranch, details.buildArchetypeLookup(archetype))
65 | )
66 | cases.add(nnkElse.newTree(nnkDiscardStmt.newTree(newEmptyNode())))
67 |
68 | return quote:
69 | proc `debugProc`(
70 | `appStatePtr`: pointer, `entityId`: EntityId
71 | ): string {.nimcall, gcsafe, raises: [Exception].} =
72 | let `appStateIdent` {.used.} = cast[ptr `appType`](`appStatePtr`)
73 | let `entityIndex` {.used.} = `appStateIdent`.`worldIdent`[`entityId`]
74 |
75 | if unlikely(`entityIndex` == nil):
76 | return "No such entity: " & $`entityId`
77 | else:
78 | `cases`
79 |
80 | of GenerateHook.Standard:
81 | let procName = ident(name)
82 | return quote:
83 | `appStateIdent`.`procName` = newCallbackDir(`appStatePtr`, `debugProc`)
84 | else:
85 | return newEmptyNode()
86 |
87 | let entityDebugGenerator* {.compileTime.} = newGenerator(
88 | ident = "EntityDebug",
89 | interest = {Standard, Outside},
90 | generate = generateEntityDebug,
91 | worldFields = worldFields,
92 | )
93 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/deleteGen.nim:
--------------------------------------------------------------------------------
1 | import std/[tables, macros, options]
2 | import archetype, tools, systemGen, archetypeBuilder, common, tupleDirective
3 | import ../runtime/[archetypeStore, world, directives]
4 |
5 | proc deleteFields(name: string): seq[WorldField] =
6 | @[(name, bindSym("Delete"))]
7 |
8 | let entity {.compileTime.} = ident("entity")
9 | let entityIndex {.compileTime.} = ident("entityIndex")
10 |
11 | proc deleteProcName(details: GenerateContext): NimNode =
12 | return details.globalName("internalDelete")
13 |
14 | proc generateDelete(details: GenerateContext, arg: SystemArg, name: string): NimNode =
15 | ## Generates the code for deleting an entity
16 |
17 | let deleteProcName = details.deleteProcName
18 |
19 | case details.hook
20 | of Outside:
21 | let appStateTypeName = details.appStateTypeName
22 |
23 | let body =
24 | if isFastCompileMode(fastDelete):
25 | newStmtList()
26 | else:
27 | var cases: NimNode
28 | if details.archetypes.len > 0:
29 | cases = nnkCaseStmt.newTree(newDotExpr(entityIndex, ident("archetype")))
30 | for (ofBranch, archetype) in archetypeCases(details):
31 | let archIdent = archetype.ident
32 | let deleteCall = quote:
33 | del(`appStateIdent`.`archIdent`, `entityIndex`.archetypeIndex)
34 | cases.add(nnkOfBranch.newTree(ofBranch, deleteCall))
35 |
36 | cases.add(nnkElse.newTree(nnkDiscardStmt.newTree(newEmptyNode())))
37 | else:
38 | cases = newEmptyNode()
39 |
40 | let log = emitEntityTrace("Deleting ", entity)
41 |
42 | quote:
43 | let deleted = del(`appStateIdent`.`worldIdent`, `entity`)
44 | if likely(isSome(deleted)):
45 | let `entityIndex` = unsafeGet(deleted)
46 | `log`
47 | `cases`
48 |
49 | return quote:
50 | proc `deleteProcName`(
51 | `appStateIdent`: pointer, `entity`: EntityId
52 | ) {.gcsafe, raises: [], nimcall, used.} =
53 | let `appStateIdent` {.used.} = cast[ptr `appStateTypeName`](`appStateIdent`)
54 | `body`
55 |
56 | of Standard:
57 | let deleteProc = name.ident
58 | return quote:
59 | `appStateIdent`.`deleteProc` = newCallbackDir(`appStatePtr`, `deleteProcName`)
60 | else:
61 | return newEmptyNode()
62 |
63 | let deleteGenerator* {.compileTime.} = newGenerator(
64 | ident = "Delete",
65 | interest = {Standard, Outside},
66 | generate = generateDelete,
67 | worldFields = deleteFields,
68 | )
69 |
70 | proc deleteAllFields(name: string, dir: TupleDirective): seq[WorldField] =
71 | @[(name, nnkBracketExpr.newTree(bindSym("DeleteAll"), dir.asTupleType))]
72 |
73 | proc deleteAllBody(details: GenerateContext, dir: TupleDirective): NimNode =
74 | let deleteProcName = details.deleteProcName
75 | result = newStmtList()
76 | for archetype in details.archetypes:
77 | if archetype.matches(dir.filter):
78 | let archetypeIdent = archetype.ident
79 | result.add quote do:
80 | for eid in entityIds(`appStateIdent`.`archetypeIdent`):
81 | `deleteProcName`(`appStatePtr`, eid)
82 |
83 | proc generateDeleteAll(
84 | details: GenerateContext, arg: SystemArg, name: string, dir: TupleDirective
85 | ): NimNode =
86 | if isFastCompileMode(fastDeleteGen):
87 | return newEmptyNode()
88 |
89 | let deleteAllImpl = details.globalName(name)
90 |
91 | case details.hook
92 | of Outside:
93 | let appStateTypeName = details.appStateTypeName
94 | let body = details.deleteAllBody(dir)
95 | return quote:
96 | proc `deleteAllImpl`(
97 | `appStatePtr`: pointer
98 | ) {.gcsafe, raises: [ValueError], nimcall.} =
99 | let `appStateIdent` {.used.} = cast[ptr `appStateTypeName`](`appStatePtr`)
100 | `body`
101 |
102 | of Standard:
103 | let ident = name.ident
104 | return quote:
105 | `appStateIdent`.`ident` = newCallbackDir(`appStatePtr`, `deleteAllImpl`)
106 | else:
107 | return newEmptyNode()
108 |
109 | proc deleteAllNestedArgs(dir: TupleDirective): seq[RawNestedArg] =
110 | @[(newEmptyNode(), "del".ident, bindSym("Delete"))]
111 |
112 | let deleteAllGenerator* {.compileTime.} = newGenerator(
113 | ident = "DeleteAll",
114 | interest = {Standard, Outside},
115 | generate = generateDeleteAll,
116 | worldFields = deleteAllFields,
117 | nestedArgs = deleteAllNestedArgs,
118 | )
119 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/directiveArg.nim:
--------------------------------------------------------------------------------
1 | import componentDef, hashes, macros, sequtils, strutils
2 |
3 | type
4 | DirectiveArgKind* = enum
5 | ## Indicates the behavior of a directive
6 | Include
7 | Exclude
8 | Optional
9 |
10 | DirectiveArg* = ref object
11 | ## Represents a single argument within a directive. For example, in:
12 | ## `Query[(Foo, Bar, Baz)]`
13 | ## This would just represent `Foo` or `Bar` or `Baz`
14 | component*: ComponentDef
15 | isPointer*: bool
16 | kind*: DirectiveArgKind
17 | signatureCache: string
18 |
19 | proc newDirectiveArg*(
20 | component: ComponentDef, isPointer: bool, kind: DirectiveArgKind
21 | ): DirectiveArg =
22 | ## Creates a DirectiveArg
23 | return DirectiveArg(component: component, isPointer: isPointer, kind: kind)
24 |
25 | proc `$`*(arg: DirectiveArg): string =
26 | result = $arg.kind & "("
27 | if arg.isPointer:
28 | result &= "ptr "
29 | result &= arg.component.readableName & ")"
30 |
31 | proc `==`*(a, b: DirectiveArg): auto =
32 | ## Compare two Directive instances
33 | (a.isPointer == b.isPointer) and (a.component == b.component)
34 |
35 | proc `<`*(a, b: DirectiveArg): auto =
36 | ## Allow deterministic sorting of directives
37 | (a.component < b.component) or (a.isPointer < b.isPointer) or (a.kind < b.kind)
38 |
39 | proc hash*(arg: DirectiveArg): Hash = ## Generate a unique hash
40 | hash(arg.component)
41 |
42 | proc type*(def: DirectiveArg): NimNode =
43 | ## The type of this component
44 | if def.isPointer:
45 | nnkPtrTy.newTree(def.component.node)
46 | else:
47 | def.component.node
48 |
49 | proc name(arg: DirectiveArg): string =
50 | ## Creates a name to describe an arg
51 | if arg.isPointer:
52 | result = "p"
53 | case arg.kind
54 | of Include:
55 | result &= "i"
56 | of Exclude:
57 | result &= "e"
58 | of Optional:
59 | result &= "o"
60 | result &= arg.component.name
61 |
62 | proc isAccessory*(arg: DirectiveArg): bool =
63 | ## Whether this arg contains an accessory component
64 | return arg.component.isAccessory
65 |
66 | proc generateName*(args: openarray[DirectiveArg]): string =
67 | ## Creates a name to describe the given components
68 | args.toSeq.mapIt(it.name).join("_")
69 |
70 | proc comps*(args: openarray[DirectiveArg]): seq[ComponentDef] =
71 | ## Returns all the components from a set of args
72 | for arg in args:
73 | result.add(arg.component)
74 |
75 | proc addSignature*(onto: var string, arg: DirectiveArg) =
76 | ## Generate a unique ID for a component
77 | if arg.signatureCache == "":
78 | arg.signatureCache = $arg.kind
79 | if arg.isPointer:
80 | arg.signatureCache &= "p"
81 | arg.signatureCache.addSignature(arg.component)
82 | onto &= arg.signatureCache
83 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/directiveSet.nim:
--------------------------------------------------------------------------------
1 | import
2 | tables, componentDef, tupleDirective, sequtils, strutils, sets, strformat,
3 | directiveArg
4 |
5 | type DirectiveSet*[T] = ref object ## All possible directives
6 | symbol: string
7 | values: OrderedTable[T, string]
8 |
9 | proc newDirectiveSet*[T](prefix: string, values: openarray[T]): DirectiveSet[T] =
10 | ## Create a set of all directives in a set of systems
11 | result.new
12 | result.symbol = prefix & $T
13 |
14 | result.values = initOrderedTable[T, string]()
15 | var suffixes = initTable[string, int]()
16 |
17 | for value in values.toSeq.deduplicate:
18 | let name = toLowerAscii(prefix) & "_" & value.name
19 | let suffix = suffixes.mgetOrPut(name, 0)
20 | suffixes[name] = suffix + 1
21 | result.values[value] = name & "_" & $suffix
22 |
23 | proc directives*[T](directives: DirectiveSet[T]): seq[T] =
24 | ## Produce all directives
25 | directives.values.keys.toSeq
26 |
27 | iterator pairs*[T](directives: DirectiveSet[T]): tuple[name: string, value: T] =
28 | ## Produce all directives and their property names
29 | for (value, name) in directives.values.pairs:
30 | yield (name, value)
31 |
32 | proc symbol*[T](directives: DirectiveSet[T]): string =
33 | ## Returns the name of this query set
34 | directives.symbol
35 |
36 | proc `$`*[T](directives: DirectiveSet[T]): string =
37 | ## Returns the name of this query set
38 | &"{directives.symbol}({directives.directives})"
39 |
40 | proc isFulfilledBy(query: TupleDirective, components: HashSet[ComponentDef]): bool =
41 | ## Determines whether a query can be fulfilled by the given components
42 | for arg in query.args:
43 | case arg.kind
44 | of Include:
45 | if arg.component notin components:
46 | return false
47 | of Exclude:
48 | if arg.component in components:
49 | return false
50 | of Optional:
51 | discard
52 | return true
53 |
54 | proc containing*(
55 | queries: DirectiveSet[TupleDirective], components: openarray[ComponentDef]
56 | ): seq[TupleDirective] =
57 | ## Yields all queries that reference the given components
58 | let compSet = components.toHashSet
59 | for query in queries.values.keys:
60 | if query.isFulfilledBy(compSet):
61 | result.add(query)
62 |
63 | proc mentioning*(
64 | queries: DirectiveSet[TupleDirective], components: openarray[ComponentDef]
65 | ): seq[TupleDirective] =
66 | ## Yields all queries that mention the given component
67 | let compSet = components.toHashSet
68 | for query in queries.values.keys:
69 | if query.toSeq.anyIt(it in compSet):
70 | result.add(query)
71 |
72 | proc nameOf*[T](directives: DirectiveSet[T], value: T): string =
73 | ## Returns the name of a directive
74 | assert(
75 | value in directives.values,
76 | &"Directive {value} was not in directiveSet: {directives}",
77 | )
78 | directives.values[value]
79 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/dualDirective.nim:
--------------------------------------------------------------------------------
1 | import componentDef, strutils, hashes, directiveArg
2 |
3 | type DualDirective* = ref object ## A directive that contains two tuples
4 | first*: seq[DirectiveArg]
5 | second*: seq[DirectiveArg]
6 | name*: string
7 |
8 | proc newDualDir*(first: seq[DirectiveArg], second: seq[DirectiveArg]): DualDirective =
9 | ## Create a new dual directive
10 | return DualDirective(
11 | first: first, second: second, name: first.generateName & "_" & second.generateName
12 | )
13 |
14 | proc hash*(directive: DualDirective): Hash =
15 | hash(directive.first) !& hash(directive.second)
16 |
17 | proc `$`*(dir: DualDirective): string =
18 | dir.name & "((" & join(dir.first, ", ") & "):(" & join(dir.second, ", ") & "))"
19 |
20 | iterator items*(directive: DualDirective): ComponentDef =
21 | ## Produce all components in a directive
22 | for arg in directive.first:
23 | yield arg.component
24 | for arg in directive.second:
25 | yield arg.component
26 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/eventGen.nim:
--------------------------------------------------------------------------------
1 | import std/[macros, strutils, tables, sequtils]
2 | import monoDirective, common, systemGen
3 | import ../runtime/[inbox, directives]
4 |
5 | proc getSignature(node: NimNode): string =
6 | case node.kind
7 | of nnkIdent:
8 | return node.strVal
9 | of nnkSym:
10 | return node.signatureHash
11 | of nnkBracketExpr:
12 | return node.children.toSeq.mapIt(it.getSignature).join()
13 | else:
14 | node.expectKind({nnkSym})
15 |
16 | proc chooseInboxName(context, argName: NimNode, local: MonoDirective): string =
17 | context.getSignature & argName.getSignature
18 |
19 | proc inboxFields(name: string, dir: MonoDirective): seq[WorldField] =
20 | @[(name, nnkBracketExpr.newTree(bindSym("seq"), dir.argType))]
21 |
22 | proc inboxSystemArg(name: string, dir: MonoDirective): NimNode =
23 | let storageIdent = name.ident
24 | let eventType = dir.argType
25 | return quote:
26 | Inbox[`eventType`](addr `appStateIdent`.`storageIdent`)
27 |
28 | proc initInbox*(name, typ: NimNode): NimNode =
29 | ## Creates the code for initializing an inbox
30 | return quote:
31 | `appStateIdent`.`name` = newSeqOfCap[`typ`](`appStateIdent`.config.inboxSize)
32 |
33 | proc generateInbox(
34 | details: GenerateContext, arg: SystemArg, name: string, inbox: MonoDirective
35 | ): NimNode =
36 | let eventStore = name.ident
37 | case details.hook
38 | of Standard:
39 | return eventStore.initInbox(inbox.argType)
40 | of AfterActiveCheck:
41 | return quote:
42 | setLen(`appStateIdent`.`eventStore`, 0)
43 | else:
44 | return newEmptyNode()
45 |
46 | let inboxGenerator* {.compileTime.} = newGenerator(
47 | ident = "Inbox",
48 | interest = {Standard, AfterActiveCheck},
49 | chooseName = chooseInboxName,
50 | generate = generateInbox,
51 | worldFields = inboxFields,
52 | systemArg = inboxSystemArg,
53 | )
54 |
55 | proc outboxFields(name: string, dir: MonoDirective): seq[WorldField] =
56 | @[(name, nnkBracketExpr.newTree(bindSym("Outbox"), dir.argType))]
57 |
58 | proc generateOutbox(
59 | details: GenerateContext, arg: SystemArg, name: string, outbox: MonoDirective
60 | ): NimNode =
61 | case details.hook
62 | of Standard:
63 | let procName = name.ident
64 | let sendProc = outbox.sendEventProcName.internal
65 | return quote:
66 | `appStateIdent`.`procName` = newCallbackDir(`appStatePtr`, `sendProc`)
67 | else:
68 | return newEmptyNode()
69 |
70 | let outboxGenerator* {.compileTime.} = newGenerator(
71 | ident = "Outbox",
72 | interest = {Standard},
73 | generate = generateOutbox,
74 | worldFields = outboxFields,
75 | )
76 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/localGen.nim:
--------------------------------------------------------------------------------
1 | import macros, systemGen, monoDirective, common, std/strutils
2 | import ../runtime/systemVar, ../util/nimNode
3 |
4 | proc chooseLocalName(context, argName: NimNode, local: MonoDirective): string =
5 | var hash: string
6 | hash.addSignature(context)
7 | return context.symbols.join("_") & "_" & hash & "_" & argName.strVal
8 |
9 | proc worldFields(name: string, dir: MonoDirective): seq[WorldField] =
10 | @[(name, nnkBracketExpr.newTree(bindSym("SystemVarData"), dir.argType))]
11 |
12 | proc generateLocal(
13 | details: GenerateContext, arg: SystemArg, name: string, dir: MonoDirective
14 | ): NimNode =
15 | return newEmptyNode()
16 |
17 | proc systemArg(name: string, dir: MonoDirective): NimNode =
18 | let nameIdent = name.ident
19 | return quote:
20 | Local(addr `appStateIdent`.`nameIdent`)
21 |
22 | let localGenerator* {.compileTime.} = newGenerator(
23 | ident = "Local",
24 | interest = {},
25 | generate = generateLocal,
26 | chooseName = chooseLocalName,
27 | worldFields = worldFields,
28 | systemArg = systemArg,
29 | )
30 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/lookupGen.nim:
--------------------------------------------------------------------------------
1 | import macros, sequtils, tables
2 | import tupleDirective, tools, common, archetype, componentDef, systemGen
3 | import ../runtime/[world, archetypeStore, directives]
4 |
5 | let entityId {.compileTime.} = ident("entityId")
6 | let entityIndex {.compileTime.} = ident("entityIndex")
7 | let compsIdent {.compileTime.} = ident("comps")
8 | let output {.compileTime.} = ident("output")
9 |
10 | proc buildArchetypeLookup(
11 | details: GenerateContext, lookup: TupleDirective, archetype: Archetype[ComponentDef]
12 | ): NimNode =
13 | ## Builds the block of code for pulling a lookup out of a specific archetype
14 |
15 | let archetypeType = archetype.asStorageTuple
16 | let archetypeIdent = archetype.ident
17 | let convert = newConverter(archetype, lookup).name
18 |
19 | return quote:
20 | let `compsIdent` = getComps[`archetypeType`](
21 | `appStateIdent`.`archetypeIdent`, `entityIndex`.archetypeIndex
22 | )
23 | return `convert`(`compsIdent`, nil, `output`)
24 |
25 | proc worldFields(name: string, dir: TupleDirective): seq[WorldField] =
26 | @[(name, nnkBracketExpr.newTree(bindSym("Lookup"), dir.asTupleType))]
27 |
28 | proc converters(ctx: GenerateContext, dir: TupleDirective): seq[ConverterDef] =
29 | for archetype in ctx.archetypes:
30 | if archetype.matches(dir.filter):
31 | result.add(newConverter(archetype, dir))
32 |
33 | proc generate(
34 | details: GenerateContext, arg: SystemArg, name: string, lookup: TupleDirective
35 | ): NimNode =
36 | ## Generates the code for instantiating queries
37 | if isFastCompileMode(fastLookup):
38 | return newEmptyNode()
39 |
40 | let lookupProc = details.globalName(name)
41 | let tupleType = lookup.args.toSeq.asTupleType
42 |
43 | case details.hook
44 | of GenerateHook.Outside:
45 | let appStateTypeName = details.appStateTypeName
46 |
47 | var cases: NimNode = newEmptyNode()
48 | if details.archetypes.len > 0:
49 | cases = nnkCaseStmt.newTree(newDotExpr(entityIndex, ident("archetype")))
50 |
51 | # Create a case statement where each branch is one of the archetypes
52 | for (ofBranch, archetype) in archetypeCases(details):
53 | if archetype.matches(lookup.filter):
54 | cases.add(
55 | nnkOfBranch.newTree(
56 | ofBranch, details.buildArchetypeLookup(lookup, archetype)
57 | )
58 | )
59 |
60 | # Add a fall through 'else' branch for any archetypes that don't fit this lookup
61 | cases.add(nnkElse.newTree(nnkReturnStmt.newTree(newLit(false))))
62 |
63 | return quote:
64 | proc `lookupProc`(
65 | `appStateIdent`: pointer, `entityId`: EntityId, `output`: var `tupleType`
66 | ): bool {.nimcall, gcsafe, raises: [], used.} =
67 | var `appStateIdent` = cast[ptr `appStateTypeName`](`appStateIdent`)
68 | let `entityIndex` {.used.} = `appStateIdent`.`worldIdent`[`entityId`]
69 | if unlikely(`entityIndex` == nil):
70 | return false
71 | `cases`
72 |
73 | of GenerateHook.Standard:
74 | let procName = ident(name)
75 | return quote:
76 | `appStateIdent`.`procName` = newCallbackDir(`appStatePtr`, `lookupProc`)
77 | else:
78 | return newEmptyNode()
79 |
80 | let lookupGenerator* {.compileTime.} = newGenerator(
81 | ident = "Lookup",
82 | interest = {Standard, Outside},
83 | generate = generate,
84 | worldFields = worldFields,
85 | converters = converters,
86 | )
87 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/monoDirective.nim:
--------------------------------------------------------------------------------
1 | import hashes, ../util/nimNode, strutils, macros
2 |
3 | type MonoDirective* = ref object ## Parsed definition of a mono directive
4 | argType*: NimNode
5 | name*: string
6 |
7 | proc newMonoDir*(argType: NimNode): MonoDirective =
8 | ## Create a new mono directive
9 | result = new(MonoDirective)
10 | result.argType = argType
11 | result.name = argType.symbols.join("_")
12 |
13 | proc hash*(directive: MonoDirective): Hash =
14 | hash(directive.argType)
15 |
16 | proc `==`*(a, b: MonoDirective): bool =
17 | cmp(a.argType, b.argType) == 0
18 |
19 | proc `$`*(dir: MonoDirective): string =
20 | dir.argType.lispRepr
21 |
22 | proc signature*(dir: MonoDirective): string =
23 | ## Returns a stable signature representing this directive
24 | result.addSignature(dir.argType)
25 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/restoreGen.nim:
--------------------------------------------------------------------------------
1 | import std/[json, macros]
2 | import systemGen, common, ../runtime/directives
3 |
4 | proc worldFields(name: string): seq[WorldField] =
5 | @[(name, bindSym("Restore"))]
6 |
7 | let jsonArg {.compileTime.} = "json".ident
8 |
9 | proc generate(details: GenerateContext, arg: SystemArg, name: string): NimNode =
10 | let wrapperName = details.globalName(name)
11 |
12 | case details.hook
13 | of Outside:
14 | let appType = details.appStateTypeName
15 | return quote:
16 | proc `wrapperName`(
17 | `appStatePtr`: pointer, `jsonArg`: string
18 | ) {.
19 | nimcall,
20 | gcsafe,
21 | raises: [IOError, OSError, JsonParsingError, ValueError, Exception],
22 | used
23 | .} =
24 | restore(cast[ptr `appType`](`appStatePtr`), `jsonArg`)
25 |
26 | of Late:
27 | let nameIdent = name.ident
28 | return quote:
29 | `appStateIdent`.`nameIdent` = newCallbackDir(`appStatePtr`, `wrapperName`)
30 | else:
31 | return newEmptyNode()
32 |
33 | let restoreGenerator* {.compileTime.} = newGenerator(
34 | ident = "Restore",
35 | interest = {Late, Outside},
36 | generate = generate,
37 | worldFields = worldFields,
38 | )
39 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/saveGen.nim:
--------------------------------------------------------------------------------
1 | import macros, systemGen, common, ../runtime/directives
2 |
3 | proc worldFields(name: string): seq[WorldField] =
4 | @[(name, bindSym("Save"))]
5 |
6 | proc generate(details: GenerateContext, arg: SystemArg, name: string): NimNode =
7 | let saveWrapperName = details.globalName(name)
8 |
9 | case details.hook
10 | of Outside:
11 | let appType = details.appStateTypeName
12 | return quote:
13 | proc `saveWrapperName`(
14 | `appStatePtr`: pointer
15 | ): string {.raises: [IOError, OSError, ValueError, Exception], nimcall, used.} =
16 | save(cast[ptr `appType`](`appStatePtr`))
17 |
18 | of Late:
19 | let nameIdent = name.ident
20 | return quote:
21 | `appStateIdent`.`nameIdent` = newCallbackDir(`appStatePtr`, `saveWrapperName`)
22 | else:
23 | return newEmptyNode()
24 |
25 | let saveGenerator* {.compileTime.} = newGenerator(
26 | ident = "Save",
27 | interest = {Late, Outside},
28 | generate = generate,
29 | worldFields = worldFields,
30 | )
31 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/sharedGen.nim:
--------------------------------------------------------------------------------
1 | import macros, directiveSet, systemGen, monoDirective, options, common
2 | import ../runtime/systemVar
3 |
4 | proc worldFields(name: string, dir: MonoDirective): seq[WorldField] =
5 | @[(name, nnkBracketExpr.newTree(bindSym("SystemVarData"), dir.argType))]
6 |
7 | proc generateShared(
8 | details: GenerateContext, arg: SystemArg, name: string, dir: MonoDirective
9 | ): NimNode =
10 | if isFastCompileMode(fastSharedGen):
11 | return newEmptyNode()
12 |
13 | result = newStmtList()
14 | case details.hook
15 | of Standard:
16 | let varIdent = ident(name)
17 |
18 | # Fill in any values from arguments passed to the app
19 | for (inputName, inputDir) in details.inputs:
20 | if dir == inputDir:
21 | let inputIdent = inputName.ident
22 | result.add quote do:
23 | systemVar.set(Shared(addr `appStateIdent`.`varIdent`), `inputIdent`)
24 | else:
25 | discard
26 |
27 | proc systemReturn(
28 | args: DirectiveSet[SystemArg], returns: MonoDirective
29 | ): Option[NimNode] =
30 | for name, directive in args:
31 | if directive.monoDir == returns:
32 | let stateIdent = name.ident
33 | let returnCode = quote:
34 | getOrRaise(Shared(addr `appStateIdent`.`stateIdent`))
35 | return some(returnCode)
36 | return none(NimNode)
37 |
38 | proc systemArg(name: string, dir: MonoDirective): NimNode =
39 | let nameIdent = name.ident
40 | return quote:
41 | Shared(addr `appStateIdent`.`nameIdent`)
42 |
43 | let sharedGenerator* {.compileTime.} = newGenerator(
44 | ident = "Shared",
45 | interest = {Standard},
46 | generate = generateShared,
47 | systemReturn = systemReturn,
48 | worldFields = worldFields,
49 | systemArg = systemArg,
50 | )
51 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/spawnGen.nim:
--------------------------------------------------------------------------------
1 | import std/[macros, sets, macrocache, options]
2 | import
3 | tools, tupleDirective, archetype, archetypeBuilder, componentDef, common, systemGen
4 | import ../runtime/[spawn, archetypeStore, world]
5 |
6 | proc archetypes(
7 | builder: var ArchetypeBuilder[ComponentDef],
8 | systemArgs: seq[SystemArg],
9 | dir: TupleDirective,
10 | ) =
11 | builder.define(dir.comps)
12 |
13 | proc worldFields(name: string, dir: TupleDirective): seq[WorldField] =
14 | @[(name, nnkBracketExpr.newTree(bindSym("RawSpawn"), dir.asTupleType))]
15 |
16 | proc systemArg(spawnType: NimNode, name: string): NimNode =
17 | let sysIdent = name.ident
18 | return quote:
19 | `appStateIdent`.`sysIdent`.`spawnType`
20 |
21 | proc spawnSystemArg(name: string, dir: TupleDirective): NimNode =
22 | systemArg(bindSym("asSpawn"), name)
23 |
24 | proc fullSpawnSystemArg(name: string, dir: TupleDirective): NimNode =
25 | systemArg(bindSym("asFullSpawn"), name)
26 |
27 | when NimMajor >= 2:
28 | const spawnSymbols = CacheTable("NecsusSpawnSymbols")
29 | else:
30 | import std/tables
31 | var spawnSymbols {.compileTime.} = initTable[string, NimNode]()
32 |
33 | proc spawnProcName(details: GenerateContext, dir: TupleDirective): NimNode =
34 | ## Returns the symbol for a spawn proc
35 | let sig = details.globalStr(dir.signature)
36 | if sig notin spawnSymbols:
37 | spawnSymbols[sig] = genSym(nskProc, "spawn")
38 | return spawnSymbols[sig]
39 |
40 | when NimMajor >= 2:
41 | const spawnProcs = CacheTable("NecsusSpawnProcs")
42 | else:
43 | var spawnProcs {.compileTime.} = initTable[string, NimNode]()
44 |
45 | proc convertSpawnValue(
46 | archetype: Archetype[ComponentDef], dir: TupleDirective, readFrom: NimNode
47 | ): NimNode =
48 | ## Generates code for taking a tuple and converting it to the archetype in which it is being stored
49 | if archetype.hasAccessories or archetype.asTupleDir.comps != dir.comps:
50 | result = nnkTupleConstr.newTree()
51 | for component in archetype.values:
52 | if component in dir:
53 | let read = nnkBracketExpr.newTree(readFrom, dir.indexOf(component).newLit)
54 | result.add(
55 | if component.isAccessory:
56 | newCall(bindSym("some"), read)
57 | else:
58 | read
59 | )
60 | else:
61 | result.add(newCall(nnkBracketExpr.newTree(bindSym("none"), component.node)))
62 | else:
63 | result = readFrom
64 |
65 | proc buildSpawnProc(details: GenerateContext, dir: TupleDirective): NimNode =
66 | ## Builds the proc needed to execute a spawn against the given tuple
67 | let sig = details.globalStr(dir.signature)
68 | if sig in spawnProcs:
69 | return newEmptyNode()
70 |
71 | let appState = details.appStateTypeName
72 | let spawnProc = details.spawnProcName(dir)
73 | let archetype = details.archetypeFor(dir)
74 | let archIdent = archetype.ident
75 | let value = genSym(nskParam, "value")
76 | let construct = archetype.convertSpawnValue(dir, value)
77 | let log = emitEntityTrace("Spawned ", ident("result"), " of kind ", $dir)
78 | let tupleTyp = dir.asTupleType
79 |
80 | result = quote:
81 | proc `spawnProc`(
82 | appStatePtr: pointer, `value`: sink `tupleTyp`
83 | ): EntityId {.nimcall, raises: [], gcsafe.} =
84 | let `appStateIdent` = cast[ptr `appState`](appStatePtr)
85 | var newEntity = `appStateIdent`.world.newEntity
86 | var slot = newSlot(`appStateIdent`.`archIdent`, newEntity.entityId)
87 | newEntity.setArchetypeDetails(
88 | readArchetype(`appStateIdent`.`archIdent`), slot.index
89 | )
90 | result = setComp(slot, `construct`)
91 | `log`
92 |
93 | spawnProcs[sig] = true.newLit
94 |
95 | proc generate(
96 | details: GenerateContext, arg: SystemArg, name: string, dir: TupleDirective
97 | ): NimNode =
98 | if isFastCompileMode(fastSpawnGen):
99 | return newEmptyNode()
100 |
101 | case details.hook
102 | of Outside:
103 | return details.buildSpawnProc(dir)
104 | of Standard:
105 | # Check for max capacity, as we can produce a better error by doing it here versus doing it later
106 | discard maxCapacity(arg.source, dir)
107 |
108 | let spawnProc = details.spawnProcName(dir)
109 | let ident = name.ident
110 | return quote:
111 | `appStateIdent`.`ident` = newSpawn(`appStatePtr`, `spawnProc`)
112 | else:
113 | discard
114 |
115 | let spawnGenerator* {.compileTime.} = newGenerator(
116 | ident = "Spawn",
117 | interest = {Outside, Standard},
118 | generate = generate,
119 | archetype = archetypes,
120 | worldFields = worldFields,
121 | systemArg = spawnSystemArg,
122 | )
123 |
124 | let fullSpawnGenerator* {.compileTime.} = newGenerator(
125 | ident = "FullSpawn",
126 | interest = {Outside, Standard},
127 | generate = generate,
128 | archetype = archetypes,
129 | worldFields = worldFields,
130 | systemArg = fullSpawnSystemArg,
131 | )
132 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/tickIdGen.nim:
--------------------------------------------------------------------------------
1 | import macros
2 | import common, systemGen, ../runtime/directives
3 |
4 | let tickId {.compileTime.} = ident("tickId")
5 | let getTickId {.compileTime.} = ident("getTickId")
6 |
7 | proc fields(name: string): seq[WorldField] =
8 | @[(tickId.strVal, ident("uint32")), (getTickId.strVal, bindSym("TickId"))]
9 |
10 | proc sysArg(name: string): NimNode =
11 | return quote:
12 | `appStateIdent`.`getTickId`
13 |
14 | proc generate(details: GenerateContext, arg: SystemArg, name: string): NimNode =
15 | if isFastCompileMode(fastTickId):
16 | return newEmptyNode()
17 |
18 | let tickGenProc = details.globalName(name)
19 | case details.hook
20 | of Outside:
21 | let appType = details.appStateTypeName
22 | return quote:
23 | proc `tickGenProc`(
24 | `appStateIdent`: pointer
25 | ): BiggestUInt {.gcsafe, raises: [], nimcall, used.} =
26 | let `appStatePtr` = cast[ptr `appType`](`appStateIdent`)
27 | return `appStatePtr`.`tickId`
28 |
29 | of Standard:
30 | return quote:
31 | `appStateIdent`.`getTickId` = newCallbackDir(`appStatePtr`, `tickGenProc`)
32 | of LoopStart:
33 | return quote:
34 | `appStateIdent`.`tickId` += 1
35 | else:
36 | return newEmptyNode()
37 |
38 | let tickIdGenerator* {.compileTime.} = newGenerator(
39 | ident = "TickId",
40 | interest = {LoopStart, Standard, Outside},
41 | generate = generate,
42 | worldFields = fields,
43 | systemArg = sysArg,
44 | )
45 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/timeGen.nim:
--------------------------------------------------------------------------------
1 | import std/[macros, sets]
2 | import common, systemGen, ../runtime/directives
3 |
4 | let lastTime {.compileTime.} = ident("lastTime")
5 |
6 | proc deltaFields(name: string): seq[WorldField] =
7 | @[(name, bindSym("TimeDelta")), (lastTime.strVal, bindSym("BiggestFloat"))]
8 |
9 | proc generateDelta(details: GenerateContext, arg: SystemArg, name: string): NimNode =
10 | if isFastCompileMode(fastTime):
11 | return newEmptyNode()
12 |
13 | let timeDelta = name.ident
14 | let timeDeltaProc = details.globalName(name)
15 | case details.hook
16 | of Outside:
17 | let appType = details.appStateTypeName
18 | return quote:
19 | proc `timeDeltaProc`(
20 | `appStateIdent`: pointer
21 | ): BiggestFloat {.gcsafe, raises: [], nimcall, used.} =
22 | let `appStatePtr` {.used.} = cast[ptr `appType`](`appStateIdent`)
23 | return `appStatePtr`.`thisTime` - `appStatePtr`.`lastTime`
24 |
25 | of Standard:
26 | return quote:
27 | `appStateIdent`.`timeDelta` = newCallbackDir(`appStatePtr`, `timeDeltaProc`)
28 | of BeforeLoop:
29 | return quote:
30 | `appStateIdent`.`lastTime` = `appStateIdent`.`startTime`
31 | of LoopEnd:
32 | return quote:
33 | `appStateIdent`.`lastTime` = `appStateIdent`.`thisTime`
34 | else:
35 | return newEmptyNode()
36 |
37 | let deltaGenerator* {.compileTime.} = newGenerator(
38 | ident = "TimeDelta",
39 | interest = {Standard, BeforeLoop, LoopEnd, Outside},
40 | generate = generateDelta,
41 | worldFields = deltaFields,
42 | )
43 |
44 | proc elapsedFields(name: string): seq[WorldField] =
45 | @[(name, bindSym("TimeElapsed"))]
46 |
47 | proc generateElapsed(details: GenerateContext, arg: SystemArg, name: string): NimNode =
48 | if isFastCompileMode(fastTime):
49 | return newEmptyNode()
50 |
51 | let timeElapsed = name.ident
52 | let timeElapsedProc = details.globalName(name)
53 | case details.hook
54 | of Outside:
55 | let appType = details.appStateTypeName
56 | return quote:
57 | proc `timeElapsedProc`(
58 | `appStateIdent`: pointer
59 | ): BiggestFloat {.gcsafe, raises: [], nimcall, used.} =
60 | let `appStatePtr` = cast[ptr `appType`](`appStateIdent`)
61 | return `appStatePtr`.`thisTime` - `appStatePtr`.`startTime`
62 |
63 | of Standard:
64 | return quote:
65 | `appStateIdent`.`thisTime` = `appStateIdent`.`startTime`
66 | `appStateIdent`.`timeElapsed` = newCallbackDir(`appStatePtr`, `timeElapsedProc`)
67 | else:
68 | return newEmptyNode()
69 |
70 | let elapsedGenerator* {.compileTime.} = newGenerator(
71 | ident = "TimeElapsed",
72 | interest = {Standard, Outside},
73 | generate = generateElapsed,
74 | worldFields = elapsedFields,
75 | )
76 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/tools.nim:
--------------------------------------------------------------------------------
1 | import macros, options, sequtils
2 | import tupleDirective, componentDef, archetype, systemGen, directiveArg, common
3 | import ../runtime/query
4 |
5 | proc asTupleType*(components: openarray[ComponentDef]): NimNode =
6 | ## Creates a tuple type from a list of components
7 | result = nnkTupleConstr.newTree()
8 | for comp in components:
9 | result.add(comp.node)
10 |
11 | proc asTupleType*(args: openarray[DirectiveArg]): NimNode =
12 | ## Creates a tuple type from a list of components
13 | result = nnkTupleConstr.newTree()
14 | for arg in args:
15 | let componentIdent =
16 | if arg.isPointer:
17 | nnkPtrTy.newTree(arg.component.ident)
18 | else:
19 | arg.component.ident
20 | case arg.kind
21 | of Include:
22 | result.add(componentIdent)
23 | of Exclude:
24 | result.add(nnkBracketExpr.newTree(bindSym("Not"), componentIdent))
25 | of Optional:
26 | result.add(nnkBracketExpr.newTree(bindSym("Option"), componentIdent))
27 |
28 | proc asTupleType*(tupleDir: TupleDirective): NimNode =
29 | tupleDir.args.toSeq.asTupleType
30 |
31 | iterator archetypeCases*(
32 | details: GenerateContext
33 | ): tuple[ofBranch: NimNode, archetype: Archetype[ComponentDef]] =
34 | for archetype in details.archetypes:
35 | yield (archetype.idSymbol, archetype)
36 |
37 | iterator both*(a, b: auto): auto =
38 | ## Yields values from one iterator then another
39 | for item in a:
40 | yield item
41 | for item in b:
42 | yield item
43 |
44 | proc joinStrs*(args: varargs[NimNode]): NimNode =
45 | ## Joins a set of stringable nim nodes into a string
46 | if args.len == 0:
47 | result = newLit("")
48 | else:
49 | result = newEmptyNode()
50 | for arg in args:
51 | let argStr = nnkPrefix.newTree(ident("$"), arg)
52 | if result.kind == nnkEmpty:
53 | result = argStr
54 | else:
55 | result = nnkInfix.newTree(ident("&"), result, argStr)
56 |
57 | proc loggable*(node: NimNode): NimNode =
58 | node
59 |
60 | proc loggable*(str: string): NimNode =
61 | newLit(str)
62 |
63 | proc emitLog*(args: varargs[NimNode, loggable]): NimNode =
64 | ## Generates code to emit a log message
65 | let msg = args.joinStrs
66 | return quote:
67 | `appStateIdent`.config.log(`msg`)
68 |
69 | proc emitEntityTrace*(args: varargs[NimNode, loggable]): NimNode =
70 | ## Emits function call for logging an entity related event
71 | return
72 | if defined(necsusEntityTrace):
73 | emitLog(args)
74 | else:
75 | return newEmptyNode()
76 |
77 | proc emitEventTrace*(args: varargs[NimNode, loggable]): NimNode =
78 | ## Emits code needed to generate an event tracing log
79 | return
80 | if defined(necsusEventTrace):
81 | emitLog(args)
82 | else:
83 | return newEmptyNode()
84 |
85 | proc emitQueryTrace*(args: varargs[NimNode, loggable]): NimNode =
86 | ## Emits code needed to generate query tracing logs
87 | return
88 | if defined(necsusQueryTrace):
89 | emitLog(args)
90 | else:
91 | return newEmptyNode()
92 |
93 | proc emitSaveTrace*(args: varargs[NimNode, loggable]): NimNode =
94 | ## Emits code needed to generate save tracing logs
95 | return
96 | if defined(necsusSaveTrace):
97 | emitLog(args)
98 | else:
99 | return newEmptyNode()
100 |
--------------------------------------------------------------------------------
/src/necsus/compiletime/tupleDirective.nim:
--------------------------------------------------------------------------------
1 | import componentDef, hashes, sequtils, strutils, ../util/bits, directiveArg
2 |
3 | type TupleDirective* = ref object ## A directive that contains a single tuple
4 | args*: seq[DirectiveArg]
5 | name*: string
6 | filter: BitsFilter
7 |
8 | proc newTupleDir*(args: openarray[DirectiveArg]): TupleDirective =
9 | ## Create a TupleDirective
10 | return TupleDirective(args: args.toSeq, name: args.generateName)
11 |
12 | proc newTupleDir*(comps: openarray[ComponentDef]): TupleDirective =
13 | ## Create a TupleDirective
14 | var args: seq[DirectiveArg]
15 | for comp in comps:
16 | args.add(newDirectiveArg(comp, false, DirectiveArgKind.Include))
17 | return newTupleDir(args)
18 |
19 | proc `$`*(dir: TupleDirective): string =
20 | dir.name & "(" & join(dir.args, ", ") & ")"
21 |
22 | proc `readable`*(dir: TupleDirective): string =
23 | result = dir.name & "("
24 | for i, arg in dir.args:
25 | if i != 0:
26 | result &= ", "
27 | result &= $arg
28 | result &= ")"
29 |
30 | iterator items*(directive: TupleDirective): ComponentDef =
31 | ## Produce all components in a directive
32 | for arg in directive.args:
33 | yield arg.component
34 |
35 | proc comps*(directive: TupleDirective): seq[ComponentDef] =
36 | ## Produce all components in a directive
37 | directive.items.toSeq
38 |
39 | iterator args*(directive: TupleDirective): DirectiveArg =
40 | ## Produce all args in a directive
41 | for arg in directive.args:
42 | yield arg
43 |
44 | proc hash*(directive: TupleDirective): Hash =
45 | hash(directive.args)
46 |
47 | proc indexOf*(directive: TupleDirective, comp: ComponentDef): int =
48 | ## Returns the index of a component in this directive
49 | for i, arg in directive.args:
50 | if arg.component == comp:
51 | return i
52 | raise newException(KeyError, "Could not find component: " & $comp)
53 |
54 | proc contains*(directive: TupleDirective, comp: ComponentDef): bool =
55 | ## Returns the index of a component in this directive
56 | for i, arg in directive.args:
57 | if arg.component == comp:
58 | return true
59 | return false
60 |
61 | proc `==`*(a, b: TupleDirective): auto =
62 | ## Compare two Directive instances
63 | a.args == b.args
64 |
65 | proc `<`*(a, b: TupleDirective): auto =
66 | ## Compare two Directive instances
67 | if a.args.len < b.args.len:
68 | return true
69 | for i in 0 ..< b.args.len:
70 | if a.args[i] < b.args[i]:
71 | return true
72 | elif a.args[i] != b.args[i]:
73 | return false
74 | return false
75 |
76 | proc filter*(dir: TupleDirective): BitsFilter =
77 | ## Returns the filter for a tuple
78 | if dir.filter == nil:
79 | var required = Bits()
80 | var excluded = Bits()
81 | for arg in dir.args:
82 | case arg.kind
83 | of DirectiveArgKind.Include:
84 | required.incl(arg.component.uniqueId)
85 | of DirectiveArgKind.Exclude:
86 | excluded.incl(arg.component.uniqueId)
87 | of DirectiveArgKind.Optional:
88 | discard
89 | dir.filter = newFilter(required, excluded)
90 | return dir.filter
91 |
92 | proc bits*(dir: TupleDirective): Bits =
93 | ## Presents this tuple as a set of bits
94 | result = Bits()
95 | for arg in dir.args:
96 | result.incl(arg.component.uniqueId)
97 |
98 | proc signature*(dir: TupleDirective): string =
99 | ## Generates a unique ID for a tuple
100 | for arg in dir.args:
101 | result.addSignature(arg)
102 |
103 | proc hasAccessories*(dir: TupleDirective): bool =
104 | ## Whether this tuple contains any accessory components
105 | for arg in dir.args:
106 | if arg.isAccessory:
107 | return true
108 |
--------------------------------------------------------------------------------
/src/necsus/runtime/entityId.nim:
--------------------------------------------------------------------------------
1 | import std/[hashes, bitops, strformat]
2 |
3 | type EntityId* = distinct uint ## Identity of an entity
4 |
5 | const GENERATION_BITS = 16
6 |
7 | const ID_BITS = sizeof(EntityId) * 8 - GENERATION_BITS
8 |
9 | const GENERATION_MASK = high(EntityId).uint shl ID_BITS
10 |
11 | const ID_MASK = high(EntityId).uint shr GENERATION_BITS
12 |
13 | proc `==`*(a, b: EntityId): bool =
14 | ## Compare two entities
15 | a.uint == b.uint
16 |
17 | proc toInt*(entityId: EntityId): uint {.inline.} =
18 | bitand(uint(entityId), ID_MASK)
19 |
20 | proc hash*(entityId: EntityId): Hash =
21 | Hash(entityId.toInt * 7)
22 |
23 | proc generation(entityId: EntityId): uint {.inline.} =
24 | ## Returns the current generation of this entity
25 | bitand(uint(entityId), GENERATION_MASK).shr(ID_BITS)
26 |
27 | proc incGen*(entityId: EntityId): EntityId =
28 | ## Increments the generation of an entity
29 | let newgen = (entityId.generation + 1).shl(ID_BITS)
30 | return EntityId(bitor(newgen, entityId.toInt))
31 |
32 | proc `$`*(entityId: EntityId): string =
33 | ## Stringify an EntityId
34 | fmt"EntityId({entityId.generation}:{entityId.toInt})"
35 |
--------------------------------------------------------------------------------
/src/necsus/runtime/inbox.nim:
--------------------------------------------------------------------------------
1 | type
2 | SeqPtr[T] = ptr seq[T]
3 |
4 | Inbox*[T] = distinct SeqPtr[T] ## Receives events
5 |
6 | iterator items*[T](inbox: Inbox[T]): lent T {.inline.} =
7 | ## Iterate over inbox items
8 | for message in items(SeqPtr[T](inbox)[]):
9 | yield message
10 |
11 | proc len*[T](inbox: Inbox[T]): uint {.inline.} = ## The number of events in this inbox
12 | SeqPtr[T](inbox)[].len.uint
13 |
--------------------------------------------------------------------------------
/src/necsus/runtime/necsusConf.nim:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | type
4 | NecsusLogger* = proc(message: string): void {.gcsafe, raises: [].}
5 |
6 | NecsusConf* = ref object ## Used to configure
7 | entitySize*: int
8 | componentSize*: int
9 | inboxSize*: int
10 | getTime*: proc(): BiggestFloat {.gcsafe.}
11 | log*: NecsusLogger
12 | eagerAlloc*: bool
13 |
14 | proc logEcho(message: string) =
15 | when defined(necsusEchoLog):
16 | echo message
17 |
18 | proc newNecsusConf*(
19 | getTime: proc(): BiggestFloat {.gcsafe.},
20 | log: NecsusLogger,
21 | entitySize: int,
22 | componentSize: int,
23 | inboxSize: int,
24 | eagerAlloc: bool = false,
25 | ): NecsusConf =
26 | ## Create a necsus configuration
27 | NecsusConf(
28 | entitySize: entitySize,
29 | componentSize: componentSize,
30 | inboxSize: inboxSize,
31 | getTime: getTime,
32 | log: log,
33 | eagerAlloc: eagerAlloc,
34 | )
35 |
36 | proc newNecsusConf*(
37 | getTime: proc(): BiggestFloat {.gcsafe.},
38 | log: NecsusLogger,
39 | eagerAlloc: bool = false,
40 | ): NecsusConf =
41 | ## Create a necsus configuration
42 | NecsusConf(
43 | entitySize: 1_000,
44 | componentSize: 400,
45 | inboxSize: 50,
46 | getTime: getTime,
47 | log: log,
48 | eagerAlloc: eagerAlloc,
49 | )
50 |
51 | when defined(js) or defined(osx) or defined(windows) or defined(posix):
52 | import std/times
53 |
54 | let DEFAULT_ENTITY_COUNT = 1_000
55 |
56 | var firstTime = epochTime()
57 | proc elapsedTime(): BiggestFloat =
58 | BiggestFloat(epochTime() - firstTime)
59 |
60 | proc newNecsusConf*(
61 | entitySize: int = DEFAULT_ENTITY_COUNT,
62 | componentSize: int = ceilDiv(entitySize, 3),
63 | inboxSize: int = max(entitySize div 20, 20),
64 | eagerAlloc: bool = false,
65 | ): NecsusConf =
66 | ## Create a necsus configuration
67 | newNecsusConf(
68 | elapsedTime, logEcho, entitySize, componentSize, inboxSize, eagerAlloc
69 | )
70 |
--------------------------------------------------------------------------------
/src/necsus/runtime/pragmas.nim:
--------------------------------------------------------------------------------
1 | template depends*(dependencies: varargs[typed]) {.pragma.}
2 | ## Marks that a system depends on another system
3 |
4 | template startupSys*() {.pragma.}
5 | ## Marks that a system should always be added as a setup system
6 |
7 | template teardownSys*() {.pragma.}
8 | ## Marks that a system should always be added as a teardown
9 |
10 | template loopSys*() {.pragma.}
11 | ## Marks that a system should always be added as part of the standard loop
12 |
13 | template saveSys*() {.pragma.} ## Marks that a proc generates a saved value
14 |
15 | template restoreSys*() {.pragma.} ## Marks a proc that restores values from JSON
16 |
17 | template eventSys*() {.pragma.}
18 | ## Marks that a system should be triggered for a specific kind of event
19 |
20 | template instanced*() {.pragma.}
21 | ## Indicates that a system proc should be used as an initializer to create
22 | ## an instance of a system. During the primary loop, the `tick` proc is
23 | ## called on that instance.
24 |
25 | template accessory*() {.pragma.}
26 | ## Flags that a component should be attached to existing archetypes rather than creating new ones. This is a
27 | ## useful tool for reducing build times when iteration over a set of entities is inexpensive.
28 |
29 | template active*(states: varargs[typed]) {.pragma.}
30 | ## Indicates a value that must be true for a system to run
31 |
32 | template maxCapacity*(capacity: Natural) {.pragma.}
33 | ## Indicates the maximum number of entities that might exist with a specific component
34 |
--------------------------------------------------------------------------------
/src/necsus/runtime/query.nim:
--------------------------------------------------------------------------------
1 | import entityId, options, ../util/blockstore
2 |
3 | type
4 | QueryItem*[Comps: tuple] = tuple[entityId: EntityId, components: Comps]
5 | ## An individual value yielded by a query. Where `Comps` is a tuple of the components to fetch in
6 | ## this query
7 |
8 | QueryNext*[Comps: tuple] = proc(
9 | appStatePtr: pointer,
10 | state: var uint,
11 | iter: var BlockIter,
12 | eid: var EntityId,
13 | slot: var Comps,
14 | ): bool {.gcsafe, raises: [], nimcall.}
15 |
16 | QueryGetLen = proc(appState: pointer): uint {.gcsafe, raises: [], nimcall.}
17 |
18 | RawQuery*[Comps] = ref object
19 | ## Allows systems to query for entities with specific components. Where `Comps` is a tuple of
20 | ## the components to fetch in this query.
21 | appState: pointer
22 | getLen: QueryGetLen
23 | getNext: QueryNext[Comps]
24 |
25 | Query*[Comps: tuple] = distinct RawQuery[Comps]
26 | ## Allows systems to query for entities with specific components. Where `Comps` is a tuple of
27 | ## the components to fetch in this query. Does not provide access to the entity ID
28 |
29 | FullQuery*[Comps: tuple] = distinct RawQuery[Comps]
30 | ## Allows systems to query for entities with specific components. Where `Comps` is a tuple of
31 | ## the components to fetch in this query. Provides access to the EntityId
32 |
33 | AnyQuery*[Comps: tuple] = Query[Comps] | FullQuery[Comps]
34 |
35 | Not*[Comps] = distinct int8
36 | ## A query flag that indicates a component should be excluded from a query. Where `Comps` is
37 | ## the single component that should be excluded.
38 |
39 | proc asFullQuery*[Comps](rawQuery: RawQuery[Comps]): FullQuery[Comps] =
40 | FullQuery[Comps](rawQuery)
41 |
42 | proc asQuery*[Comps](rawQuery: RawQuery[Comps]): Query[Comps] =
43 | Query[Comps](rawQuery)
44 |
45 | proc newQuery*[Comps: tuple](
46 | appState: pointer, getLen: QueryGetLen, getNext: QueryNext[Comps]
47 | ): RawQuery[Comps] =
48 | RawQuery[Comps](appState: appState, getLen: getLen, getNext: getNext)
49 |
50 | iterator pairs*[Comps: tuple](query: FullQuery[Comps]): QueryItem[Comps] =
51 | ## Iterates through the entities in a query
52 | let raw = RawQuery[Comps](query)
53 | var state: uint
54 | var iter: BlockIter
55 | var eid: EntityId
56 | var slot: Comps
57 | while raw.getNext(raw.appState, state, iter, eid, slot):
58 | yield (eid, slot)
59 |
60 | iterator items*[Comps: tuple](query: AnyQuery[Comps]): Comps =
61 | ## Iterates through the entities in a query
62 | let raw = RawQuery[Comps](query)
63 | var state: uint
64 | var iter: BlockIter
65 | var eid: EntityId
66 | var slot: Comps
67 | while raw.getNext(raw.appState, state, iter, eid, slot):
68 | yield slot
69 |
70 | proc len*[Comps: tuple](query: AnyQuery[Comps]): uint {.gcsafe, raises: [].} =
71 | ## Returns the number of entities in this query
72 | let rawQuery = RawQuery[Comps](query)
73 | return rawQuery.getLen(rawQuery.appState)
74 |
75 | proc single*[Comps: tuple](query: AnyQuery[Comps]): Option[Comps] =
76 | ## Returns a single element from a query
77 | for comps in query:
78 | return some(comps)
79 |
--------------------------------------------------------------------------------
/src/necsus/runtime/spawn.nim:
--------------------------------------------------------------------------------
1 | import entityId, world, archetypeStore, ../util/tools, std/macros
2 |
3 | type
4 | RawSpawn*[C: tuple] = ref object ## A callback for populating a component with values
5 | app: pointer
6 | callback:
7 | proc(app: pointer, value: sink C): EntityId {.nimcall, raises: [], gcsafe.}
8 |
9 | Spawn*[C: tuple] = distinct RawSpawn[C]
10 | ## Describes a type that is able to create new entities. Where `C` is a tuple
11 | ## with all the components to initially attach to this entity. Does not return the new EntityId
12 |
13 | FullSpawn*[C: tuple] = distinct RawSpawn[C]
14 | ## Describes a type that is able to create new entities. Where `C` is a tuple
15 | ## with all the components to initially attach to this entity. Returns the new EntityId
16 |
17 | proc asFullSpawn*[Comps](rawSpawn: RawSpawn[Comps]): FullSpawn[Comps] =
18 | FullSpawn[Comps](rawSpawn)
19 |
20 | proc asSpawn*[Comps](rawSpawn: RawSpawn[Comps]): Spawn[Comps] =
21 | Spawn[Comps](rawSpawn)
22 |
23 | proc newSpawn*[Comps: tuple](
24 | app: pointer,
25 | callback:
26 | proc(app: pointer, value: sink Comps): EntityId {.nimcall, raises: [], gcsafe.},
27 | ): RawSpawn[Comps] =
28 | return RawSpawn[Comps](app: app, callback: callback)
29 |
30 | proc beginSpawn*[Comps: tuple](
31 | world: var World, store: ptr ArchetypeStore[Comps]
32 | ): NewArchSlot[Comps] {.inline, gcsafe, raises: [].} =
33 | ## Spawns an entity in this archetype
34 | var newEntity = world.newEntity
35 | result = store.newSlot(newEntity.entityId)
36 | newEntity.setArchetypeDetails(store.archetype, result.index)
37 |
38 | when isSinkMemoryCorruptionFixed():
39 | proc set[C: tuple](
40 | spawn: RawSpawn[C], values: sink C
41 | ): EntityId {.raises: [], inline.} =
42 | return spawn.callback(spawn.app, values)
43 |
44 | else:
45 | proc set[C: tuple](spawn: RawSpawn[C], values: C): EntityId {.raises: [], inline.} =
46 | return spawn.callback(spawn.app, values)
47 |
48 | proc set*[C: tuple](spawn: Spawn[C], values: sink C) {.raises: [], inline.} =
49 | ## Spawns an entity with the given components
50 | discard set(RawSpawn[C](spawn), values)
51 |
52 | proc set*[C: tuple](spawn: FullSpawn[C], values: sink C): EntityId {.inline.} =
53 | ## Spawns an entity with the given components
54 | return set(RawSpawn[C](spawn), values)
55 |
56 | macro buildTuple(values: varargs[untyped]): untyped =
57 | result = nnkTupleConstr.newTree()
58 | for elem in values:
59 | result.add(elem)
60 |
61 | template with*[C: tuple](spawn: Spawn[C], values: varargs[typed]) =
62 | ## spawns the given values
63 | set(spawn, buildTuple(values))
64 |
65 | template with*[C: tuple](spawn: FullSpawn[C], values: varargs[typed]): EntityId =
66 | ## spawns the given values
67 | set(spawn, buildTuple(values))
68 |
--------------------------------------------------------------------------------
/src/necsus/runtime/systemVar.nim:
--------------------------------------------------------------------------------
1 | import options
2 |
3 | type
4 | SystemVarData*[T] = object ## A system variable
5 | value: Option[T]
6 |
7 | Shared*[T] = distinct ptr SystemVarData[T]
8 | ## Wrapper around data that is shared across all systems
9 |
10 | SharedOrT*[T] = Shared[T] | T ## A shared value or the value itself
11 |
12 | Local*[T] = distinct ptr SystemVarData[T]
13 | ## Wrapper around data that is specific to a single system
14 |
15 | LocalOrT*[T] = Local[T] | T ## A local value or the value itself
16 |
17 | SystemVar*[T] = Shared[T] | Local[T]
18 |
19 | proc extract[T](sysvar: SystemVar[T]): ptr SystemVarData[T] {.inline.} =
20 | cast[ptr SystemVarData[T]](sysvar)
21 |
22 | proc clear*[T](sysvar: SystemVar[T]) {.inline.} =
23 | ## Unsets the value in a system variable
24 | sysvar.extract.value = none(T)
25 |
26 | proc isEmpty*[T](sysvar: SystemVar[T]): bool {.inline.} =
27 | ## Returns whether a system variable has a value
28 | sysvar.extract.value.isNone
29 |
30 | proc isSome*[T](sysvar: SystemVar[T]): bool {.inline.} =
31 | ## Returns whether a system variable has a value
32 | not isEmpty(sysVar)
33 |
34 | proc set*[T](sysvar: SystemVar[T], value: sink T) {.inline.} =
35 | ## Sets the value in a system variable
36 | sysvar.extract.value = some(value)
37 |
38 | proc `:=`*[T](sysvar: SystemVar[T], value: sink T) {.inline.} =
39 | ## Sets the value in a system variable
40 | set(sysvar, value)
41 |
42 | proc getOrRaise*[T](sysvar: SystemVar[T]): var T {.inline.} =
43 | ## Returns the value in a system variable
44 | sysvar.extract.value.get()
45 |
46 | template getOrPut*[T](sysvar: SystemVar[T], build: typed): var T =
47 | ## Returns the value in a system variable
48 | if sysvar.isEmpty:
49 | sysvar := build
50 | sysvar.getOrRaise
51 |
52 | proc getOrPut*[T](sysvar: SystemVar[T]): var T =
53 | ## Returns the value in a system variable
54 | return getOrPut(sysvar, default(T))
55 |
56 | proc get*[T](sysvar: SystemVar[T], default: T): T {.inline.} =
57 | ## Returns the value in a system variable
58 | sysvar.extract.value.get(default)
59 |
60 | proc get*[T](sysvar: SystemVar[T]): T {.inline.} =
61 | ## Returns the value in a system variable
62 | return sysvar.get(
63 | when T is string:
64 | ""
65 | elif T is SomeNumber:
66 | 0
67 | elif compiles(get(sysvar, {})):
68 | {}
69 | elif T is seq:
70 | @[]
71 | else:
72 | default(T)
73 | )
74 |
75 | proc `==`*[T](sysvar: SystemVar[T], value: T): bool {.inline.} =
76 | ## Returns whether a sysvar is set and equals the given value
77 | sysvar.extract.value == some(value)
78 |
79 | proc unwrap*[T](sysvar: SharedOrT[T] | LocalOrT[T]): T {.inline.} =
80 | ## Pulls a value out of a `SystemVar` or raises
81 | return when sysvar is T: sysvar else: sysvar.getOrRaise
82 |
83 | proc unwrap*[T](sysvar: SharedOrT[T] | LocalOrT[T], otherwise: T): T {.inline.} =
84 | ## Pulls a value out of a `SystemVar` or raises
85 | return
86 | when sysvar is T:
87 | sysvar
88 | else:
89 | sysvar.get(otherwise)
90 |
91 | proc `$`*[T](sysvar: SystemVar[T]): string =
92 | $sysvar.extract.value
93 |
94 | iterator items*[T](sysvar: var SystemVar[T]): var T =
95 | if sysvar.isSome:
96 | yield sysvar.extract.value.get()
97 |
98 | iterator items*[T](sysvar: SystemVar[T]): lent T =
99 | if sysvar.isSome:
100 | yield sysvar.extract.value.get()
101 |
102 | template `from`*[T](variable: untyped, source: SystemVar[T]): bool =
103 | ## Reads a value from a `SystemVar`, assigning it to a value and returning true if it exists. This allows
104 | ## you to check for the presence of a value and assign it to a variable in one step
105 | source.isSome and (let variable: T = source.extract.value.unsafeGet; true)
106 |
--------------------------------------------------------------------------------
/src/necsus/runtime/tuples.nim:
--------------------------------------------------------------------------------
1 | import std/[options, macros, algorithm, sequtils], ../util/[typeReader, nimNode]
2 |
3 | proc getTupleSubtypes(typ: NimNode): seq[NimNode] =
4 | let resolved = typ.resolveTo({nnkTupleConstr, nnkTupleTy, nnkCall}).get(typ)
5 | case resolved.kind
6 | of nnkTupleConstr:
7 | result = resolved.children.toSeq
8 | of nnkTupleTy:
9 | for child in resolved:
10 | child.expectKind(nnkIdentDefs)
11 | result.add(child[1])
12 | of nnkCall:
13 | if resolved[0].strval == "extend":
14 | result = resolved[1].getTupleSubtypes() & resolved[2].getTupleSubtypes()
15 | else:
16 | error("Unable to resolve tuple type for " & resolved.repr, resolved)
17 | else:
18 | resolved.expectKind({nnkTupleConstr, nnkTupleTy, nnkSym})
19 |
20 | macro extend*(tuples: varargs[typed]): typedesc =
21 | ## Combines tuples type definitions to create a new tuple type
22 | var subtypes: seq[NimNode]
23 | for tup in tuples:
24 | subtypes.add(tup.getTupleSubtypes())
25 |
26 | subtypes.sort(nimNode.cmp)
27 |
28 | result = nnkTupleConstr.newTree(subtypes)
29 | result.copyLineInfo(tuples[0])
30 |
31 | proc `as`*[T: tuple](value: T, typ: typedesc): T =
32 | ## Casts a value to a type and returns it. Used for joining tuples
33 | static:
34 | assert(typ is T)
35 | value
36 |
37 | proc getTupleData(tup: NimNode): tuple[typ: NimNode, construct: NimNode] =
38 | case tup.kind
39 | of nnkInfix:
40 | if tup[0].strVal != "as":
41 | error("Expecting an 'as' infix operator", tup[0])
42 | return (typ: tup[2], construct: tup[1])
43 | of nnkTupleConstr:
44 | return (typ: tup.getTypeInst, construct: tup)
45 | else:
46 | tup.expectKind({nnkInfix, nnkTupleConstr})
47 |
48 | macro join*(exprs: varargs[typed]): untyped =
49 | ## Combines two tuple values into a single tuple value according to the sorting
50 | ## rules for archetype component types
51 |
52 | exprs.expectKind(nnkBracket)
53 |
54 | var lets = nnkLetSection.newTree()
55 | var children: seq[(NimNode, int, NimNode)]
56 |
57 | for tup in exprs:
58 | let (tupleType, tupleConstruct) = tup.getTupleData()
59 |
60 | let thisVar = genSym(nskLet, "temp")
61 | lets.add(nnkIdentDefs.newTree(thisVar, tupleType, tupleConstruct))
62 |
63 | let tupleSubs = tupleType.getTupleSubtypes()
64 | for i, child in tupleSubs:
65 | children.add((thisVar, i, child))
66 |
67 | children.sort do(a, b: (NimNode, int, NimNode)) -> int:
68 | return nimNode.cmp(a[2], b[2])
69 |
70 | var output = nnkTupleConstr.newTree()
71 | for (source, idx, _) in children:
72 | output.add(nnkBracketExpr.newTree(source, newLit(idx)))
73 |
74 | return newStmtList(lets, output)
75 |
--------------------------------------------------------------------------------
/src/necsus/runtime/world.nim:
--------------------------------------------------------------------------------
1 | import entityId, ../util/blockstore, std/[deques, options]
2 |
3 | type
4 | ArchetypeId* = distinct BiggestInt
5 |
6 | EntityIndex* = object
7 | entityId*: EntityId
8 | archetype*: ArchetypeId
9 | archetypeIndex*: uint
10 |
11 | NewEntity* = distinct ptr EntityIndex
12 |
13 | World* = object ## Contains the data describing the entire world
14 | nextEntityId: uint
15 | entityIds: Deque[EntityId]
16 | index: seq[EntityIndex]
17 |
18 | proc newWorld*(initialSize: SomeInteger): World =
19 | ## Creates a new world
20 | World(
21 | entityIds: initDeque[EntityId](initialSize div 10),
22 | index: newSeq[EntityIndex](initialSize),
23 | )
24 |
25 | proc getNewEntityId*(world: var World): EntityId {.inline.} =
26 | if world.entityIds.len > 0:
27 | result = world.entityIds.popFirst().incGen
28 | else:
29 | result = EntityId(world.nextEntityId)
30 | inc world.nextEntityId
31 |
32 | proc newEntity*(world: var World): NewEntity =
33 | ## Constructs a new entity and invokes
34 | let eid = world.getNewEntityId()
35 | let entry = addr world.index[eid.toInt]
36 | entry.entityId = eid
37 | return NewEntity(entry)
38 |
39 | proc entityId*(newEntity: NewEntity): EntityId =
40 | ## Returns the entity ID of a newly created entity
41 | (ptr EntityIndex)(newEntity).entityId
42 |
43 | proc setArchetypeDetails*(entry: NewEntity, archetype: ArchetypeId, index: uint) =
44 | ## Stores the archetype details about an entity
45 | let entry = (ptr EntityIndex)(entry)
46 | entry.archetype = archetype
47 | entry.archetypeIndex = index
48 |
49 | proc `[]`*(world: World, entityId: EntityId): ptr EntityIndex =
50 | ## Look up entity information based on an entity ID
51 | result = unsafeAddr world.index[entityId.toInt]
52 | if unlikely(result.entityId != entityId):
53 | result = nil
54 |
55 | proc del*(world: var World, entityId: EntityId): Option[EntityIndex] =
56 | ## Deletes an entity and returns the archetype and index that also needs to be deleted
57 | let entry = world.index[entityId.toInt]
58 | if likely(entry.entityId == entityId):
59 | world.index[entityId.toInt] = default(EntityIndex)
60 | world.entityIds.addLast(entityId)
61 | return some(entry)
62 |
--------------------------------------------------------------------------------
/src/necsus/util/blockstore.nim:
--------------------------------------------------------------------------------
1 | import std/[deques, options]
2 |
3 | type
4 | EntryData[V] = object
5 | idx: uint
6 | alive: bool
7 | value: V
8 |
9 | Entry*[V] = ptr EntryData[V]
10 |
11 | BlockStore*[V] = ref object ## Stores a block of packed values
12 | nextId: uint
13 | hasRecycledValues: bool
14 | recycle: Deque[uint]
15 | data: seq[EntryData[V]]
16 | len: uint
17 |
18 | BlockIter* = object
19 | index: uint
20 | isDone: bool
21 |
22 | proc newBlockStore*[V](size: SomeInteger): BlockStore[V] =
23 | ## Instantiates a new BlockStore
24 | BlockStore[V](
25 | recycle: initDeque[uint](size.int div 2), data: newSeq[EntryData[V]](size)
26 | )
27 |
28 | proc isFirst*(iter: BlockIter): bool =
29 | iter.index == 0
30 |
31 | proc isDone*(iter: BlockIter): bool {.inline.} =
32 | iter.isDone
33 |
34 | func len*[V](blockstore: var BlockStore[V]): uint =
35 | ## Returns the length of this blockstore
36 | blockstore.len
37 |
38 | proc reserve*[V](blockstore: var BlockStore[V]): Entry[V] =
39 | ## Reserves a slot for a value
40 | var index: uint
41 |
42 | block indexBreak:
43 | if blockstore.hasRecycledValues:
44 | if blockstore.recycle.len > 0:
45 | index = blockstore.recycle.popFirst()
46 | break indexBreak
47 | blockstore.hasRecycledValues = false
48 | index = blockstore.nextId
49 | blockstore.nextId += 1
50 |
51 | if unlikely(index >= blockstore.data.len.uint):
52 | raise newException(IndexDefect, "Storage capacity exceeded: " & $index)
53 |
54 | blockstore.len += 1
55 | result = addr blockstore.data[index]
56 | result.idx = index
57 |
58 | proc index*[V](entry: Entry[V]): uint {.inline.} = ## Returns the index of an entry
59 | entry.idx
60 |
61 | template value*[V](entry: Entry[V]): var V = ## Returns the value of an entry
62 | entry.value
63 |
64 | proc commit*[V](entry: Entry[V]) {.inline.} =
65 | ## Marks that an entry is ready to be used
66 | entry.alive = true
67 |
68 | template set*[V](entry: Entry[V], newValue: V) =
69 | ## Sets a value on an entry
70 | entry.value = newValue
71 | entry.commit
72 |
73 | template push*[V](store: var BlockStore[V], newValue: V): uint =
74 | ## Adds a value and returns an index to it
75 | var entry = store.reserve
76 | entry.set(newValue)
77 | entry.index
78 |
79 | proc del*[V](store: var BlockStore[V], idx: uint): V =
80 | ## Deletes a field
81 | if store.data[idx].alive:
82 | store.data[idx].alive = false
83 | store.len -= 1
84 | let deleted = move(store.data[idx])
85 | result = deleted.value
86 | store.recycle.addLast(idx)
87 | store.hasRecycledValues = true
88 |
89 | proc `[]`*[V](store: BlockStore[V], idx: uint): var V =
90 | ## Reads a field
91 | store.data[idx].value
92 |
93 | template `[]=`*[V](store: BlockStore[V], idx: uint, newValue: V) =
94 | ## Sets a new value for a key
95 | store.data[idx].value = newValue
96 |
97 | proc next*[V](store: var BlockStore[V], iter: var BlockIter): ptr V {.inline.} =
98 | ## Returns the next value in an iterator
99 | while true:
100 | if unlikely(store == nil or iter.index >= store.nextId):
101 | iter.isDone = true
102 | return nil
103 | elif likely(store.data[iter.index].alive):
104 | iter.index += 1
105 | return addr store.data[iter.index - 1].value
106 | else:
107 | iter.index += 1
108 |
109 | iterator items*[V](store: var BlockStore[V]): var V =
110 | ## Iterate through all values in this BlockStore
111 | var iter: BlockIter
112 | var value: ptr V
113 | while true:
114 | value = store.next(iter)
115 | if value == nil:
116 | break
117 | yield value[]
118 |
--------------------------------------------------------------------------------
/src/necsus/util/dump.nim:
--------------------------------------------------------------------------------
1 | import std/[macros, strutils, sets, tables]
2 | import ../compiletime/[parse, systemGen]
3 |
4 | proc modulePath(node: NimNode): string =
5 | ## Attempts to determine if there is a full path available for a given module
6 | if node.lineInfoObj.filename.startsWith(getProjectPath()):
7 | result = node.lineInfoObj.filename
8 | result.removePrefix(getProjectPath())
9 | result.removePrefix("/")
10 | result.removeSuffix(".nim")
11 |
12 | proc getModule(node: NimNode): string =
13 | ## Returns the module path for a nim node
14 | when NimMajor >= 2:
15 | {.push warning[Deprecated]: off.}
16 |
17 | case node.kind
18 | of nnkTypeDef, nnkPragmaExpr, nnkProcDef, nnkIdentDefs:
19 | return node[0].getModule()
20 | of nnkSym:
21 | let modulePath = node.modulePath
22 | if modulePath != "":
23 | return modulePath
24 |
25 | let ownerModule = node.owner.modulePath
26 | if ownerModule != "":
27 | return ownerModule
28 |
29 | let owner = node.owner
30 | if owner == node:
31 | return node.strVal
32 | elif owner.kind != nnkNilLit:
33 | let parent = owner.getModule
34 | if parent == "":
35 | return owner.strVal
36 | else:
37 | return parent & "/" & owner.strVal
38 | else:
39 | return ""
40 |
41 | when NimMajor >= 2:
42 | {.pop.}
43 |
44 | proc collectImports(node: NimNode, into: var HashSet[string]) =
45 | case node.kind
46 | of nnkSym:
47 | into.incl(node.getImpl.getModule())
48 | of nnkIdentDefs:
49 | node[1].collectImports(into)
50 | of nnkBracketExpr, nnkTupleTy:
51 | for child in node.children:
52 | child.collectImports(into)
53 | else:
54 | discard
55 |
56 | proc collectImports(nodes: openarray[NimNode], into: var HashSet[string]) =
57 | for node in nodes:
58 | collectImports(node, into)
59 |
60 | proc dumpImports(app: ParsedApp, systems: openarray[ParsedSystem]) =
61 | var imports = initHashSet[string]()
62 | for component in app.components:
63 | component.node.collectImports(imports)
64 |
65 | for system in systems:
66 | system.symbol.collectImports(imports)
67 | system.prefixArgs.collectImports(imports)
68 | for arg in system.allArgs:
69 | for node in arg.nodes:
70 | node.collectImports(imports)
71 |
72 | for moduleName in imports:
73 | if moduleName != "":
74 | echo "import ", moduleName, " {.all.}"
75 |
76 | proc fixVarNames(node: NimNode, symbols: var TableRef[string, NimNode]): NimNode =
77 | ## Replaces any variable names that start with ':' with a copy/pastable name
78 | if node.kind == nnkSym and node.strVal.startsWith(':'):
79 | return symbols.mgetOrPut(node.strVal, genSym(nskLet, "tempValue"))
80 | elif node.len > 0:
81 | result = node.kind.newTree()
82 | for child in node:
83 | result.add(child.fixVarNames(symbols))
84 | else:
85 | return node
86 |
87 | proc fixTempVars(output: NimNode): NimNode =
88 | ##
89 | ## Fixes situations where Nim produces invalid code, like this:
90 | ##
91 | ## appState.config =
92 | ## let :tmp = 10000
93 | ## newNecsusConf(:tmp, ceilDiv(:tmp, 3))
94 | ##
95 | case output.kind
96 | of nnkAsgn:
97 | if output[1].kind != nnkStmtListExpr:
98 | return output
99 | var repaired = newStmtList()
100 | repaired.add(output[1][0 ..< 1])
101 | repaired.add(nnkAsgn.newTree(output[0], output[1][^1]))
102 | var symbols = newTable[string, NimNode]()
103 | return repaired.fixVarNames(symbols)
104 | of nnkStmtList, nnkProcDef:
105 | result = output.kind.newTree
106 | for child in output:
107 | result.add(child.fixTempVars())
108 | else:
109 | return output
110 |
111 | proc dumpGeneratedCode*(
112 | output: NimNode, app: ParsedApp, systems: openarray[ParsedSystem]
113 | ) =
114 | ## Prints the generated necsus app for debugging purposes
115 | echo "import std/[math, json, jsonutils, options, importutils]"
116 | echo "import necsus/runtime/[world, archetypeStore], necsus/util/[profile, tools, blockstore]"
117 | dumpImports(app, systems)
118 |
119 | echo "const DEFAULT_ENTITY_COUNT = 1_000"
120 | var line: string
121 | for rawLine in output.fixTempVars().repr.splitLines():
122 | line &=
123 | rawLine
124 | .replace("proc =destroy", "proc `=destroy`")
125 | .replace("proc =copy", "proc `=copy`")
126 | .replace("proc =copy", "proc `=copy`")
127 | .replace("`gensym", "_gensym")
128 | while "__" in line:
129 | line = line.replace("__", "_")
130 |
131 | if line.endsWith("addr") or line.endsWith("sink"):
132 | line &= " "
133 | else:
134 | echo line.strip(leading = false)
135 | line = ""
136 |
137 | echo line
138 |
--------------------------------------------------------------------------------
/src/necsus/util/nimNode.nim:
--------------------------------------------------------------------------------
1 | import macros, strformat, sequtils, hashes
2 |
3 | proc symbols*(node: NimNode): seq[string] =
4 | ## Extracts all the symbols from a NimNode tree
5 | case node.kind
6 | of nnkSym, nnkIdent, nnkStrLit .. nnkTripleStrLit:
7 | return @[node.strVal]
8 | of nnkCharLit .. nnkUInt64Lit:
9 | return @[$node.intVal]
10 | of nnkFloatLit .. nnkFloat64Lit:
11 | return @[$node.floatVal]
12 | of nnkNilLit:
13 | return @["nil"]
14 | of nnkBracketExpr, nnkTupleTy, nnkTupleConstr:
15 | return node.toSeq.mapIt(it.symbols).foldl(concat(a, b))
16 | of nnkIdentDefs:
17 | return concat(node[0].symbols, node[1].symbols)
18 | of nnkRefTy:
19 | return concat(@["ref"], node[0].symbols)
20 | else:
21 | error(&"Unable to generate a component symbol from node ({node.kind}): {node.repr}")
22 |
23 | proc hash*(node: NimNode): Hash =
24 | ## Generates a unique hash for a NimNode
25 | case node.kind
26 | of nnkSym, nnkIdent, nnkStrLit .. nnkTripleStrLit:
27 | return hash(node.strVal)
28 | of nnkCharLit .. nnkUInt64Lit:
29 | return hash(node.intVal)
30 | of nnkFloatLit .. nnkFloat64Lit:
31 | return hash(node.floatVal)
32 | of nnkNilLit, nnkEmpty:
33 | return hash(0)
34 | of nnkBracketExpr, nnkTupleTy, nnkIdentDefs, nnkTupleConstr:
35 | return node.toSeq.mapIt(hash(it)).foldl(a !& b, hash(node.kind))
36 | of nnkRefTy:
37 | return hash(node[0])
38 | else:
39 | error(&"Unable to generate a hash from node ({node.kind}): {node.repr}")
40 |
41 | proc cmp*(a: NimNode, b: NimNode): int =
42 | ## Compare two nim nodes for sorting
43 | if a == b:
44 | return 0
45 |
46 | if a.kind == nnkSym and b.kind == nnkSym:
47 | let nameCompare = cmp(a.strVal, b.strVal)
48 | if nameCompare == 0:
49 | return cmp(a.signatureHash, b.signatureHash)
50 | else:
51 | return nameCompare
52 | elif a.kind in {nnkSym, nnkIdent} and b.kind in {nnkSym, nnkIdent}:
53 | return cmp(a.strVal, b.strVal)
54 | elif a.kind != b.kind:
55 | return cmp(a.kind, b.kind)
56 | elif a.len != b.len:
57 | return cmp(a.len, b.len)
58 | else:
59 | for i in 0 ..< a.len:
60 | let compared = cmp(a[i], b[i])
61 | if compared != 0:
62 | return compared
63 | return 0
64 |
65 | proc addSignature*(onto: var string, comp: NimNode) =
66 | ## Generate a unique ID for a component
67 | case comp.kind
68 | of nnkSym:
69 | onto &= comp.signatureHash
70 | of nnkBracketExpr, nnkTupleConstr, nnkTupleTy:
71 | for child in comp.children:
72 | onto.addSignature(child)
73 | of nnkIdentDefs:
74 | onto.addSignature(comp[1])
75 | else:
76 | comp.expectKind({nnkSym})
77 |
--------------------------------------------------------------------------------
/src/necsus/util/profile.nim:
--------------------------------------------------------------------------------
1 | import algorithm, sequtils, strformat, strutils, ../runtime/necsusConf
2 |
3 | const READINGS = 600'u
4 |
5 | type Profiler* = object
6 | name*: string
7 | next: uint
8 | readings: array[READINGS, BiggestFloat]
9 |
10 | proc record*(profiler: var Profiler, time: BiggestFloat) =
11 | ## Records a reading
12 | profiler.readings[profiler.next mod READINGS] = time
13 | profiler.next += 1
14 |
15 | proc format(seconds: BiggestFloat): string =
16 | formatBiggestFloat(seconds * 1_000_000, ffDecimal, 3) & " μs"
17 |
18 | proc summarize*(profilers: var openarray[Profiler], conf: NecsusConf) =
19 | var slowest: seq[(BiggestFloat, BiggestFloat, string)]
20 | for profiler in profilers.mitems:
21 | if profiler.next mod READINGS == 0 and profiler.next > 0:
22 | profiler.readings.sort()
23 | if profiler.readings[READINGS div 2] > 0:
24 | let median = profiler.readings[READINGS div 2]
25 | let average = foldl(profiler.readings, a + b) / READINGS.BiggestFloat
26 | slowest.add((median, average, profiler.name))
27 |
28 | if slowest.len > 0:
29 | for (median, average, name) in slowest.sortedByIt(it[0]).reversed():
30 | conf.log(
31 | fmt(
32 | "Profile -- med: {format(median):>10}, avg: {format(average):>10} -- {name}"
33 | )
34 | )
35 |
--------------------------------------------------------------------------------
/src/necsus/util/tools.nim:
--------------------------------------------------------------------------------
1 | import std/options
2 |
3 | proc isSinkMemoryCorruptionFixed*(): bool =
4 | ## Returns whether the current version of Nim has a fixed implementation of
5 | ## the 'sink' parameter that doesn't cause memory corruption.
6 | ## See https://github.com/nim-lang/Nim/issues/23907
7 | return false
8 |
9 | proc stringify*[T](value: T): string {.raises: [], gcsafe.} =
10 | ## Converts a value to a string as best as it can
11 | try:
12 | when compiles($value):
13 | return $value
14 | elif compiles(value.repr):
15 | return value.repr
16 | else:
17 | return $T
18 | except:
19 | return $T & "(Failed to generate string)"
20 |
21 | template optionPtr*[T](opt: Option[T]): Option[ptr T] =
22 | ## Returns a pointer to a value in an option
23 | if opt.isSome:
24 | some(unsafeAddr opt.unsafeGet)
25 | else:
26 | none(ptr T)
27 |
--------------------------------------------------------------------------------
/src/necsus/util/typeReader.nim:
--------------------------------------------------------------------------------
1 | import macros, options, tables
2 |
3 | proc isPragma*(found, expect: NimNode): bool =
4 | ## Determines whether a NimNode represents the given pragma
5 | expect.expectKind({nnkSym})
6 | case found.kind
7 | of nnkSym:
8 | return found == expect
9 | of nnkCall:
10 | return found[0].isPragma(expect)
11 | of nnkIdent:
12 | return false
13 | of nnkExprColonExpr:
14 | return found[0].isPragma(expect)
15 | else:
16 | error("Unable to extract pragma from " & found.lispRepr, found)
17 |
18 | proc findPragma*(node: NimNode): NimNode =
19 | ## Finds the pragma node attached to a nim node
20 | case node.kind
21 | of nnkSym:
22 | if node.symKind == nskType:
23 | return node.getImpl.findPragma
24 | of RoutineNodes:
25 | return node.pragma
26 | of nnkIdentDefs, nnkTypeDef:
27 | if node[0].kind == nnkPragmaExpr:
28 | return node[0][1]
29 | else:
30 | discard
31 | return newEmptyNode()
32 |
33 | proc hasPragma*(node, pragma: NimNode): bool =
34 | ## Determines whether a node has a given pragma
35 | let pragmaSet = node.findPragma
36 | if pragmaSet.kind == nnkPragma:
37 | for child in pragmaSet:
38 | if child.isPragma(pragma):
39 | return true
40 |
41 | proc findChildSyms*(node: NimNode, output: var seq[NimNode]) =
42 | ## Finds all symbols in the children of a node and returns them
43 | if node.kind == nnkSym:
44 | output.add(node)
45 | elif node.kind == nnkEmpty:
46 | discard
47 | elif node.len == 0:
48 | error("Expecting a system symbol, but got: " & node.repr, node)
49 | else:
50 | for child in node.children:
51 | findChildSyms(child, output)
52 |
53 | proc asGenericTable(
54 | genericParams: NimNode, values: openArray[NimNode]
55 | ): Table[string, NimNode] =
56 | ## Creates a table of generic parameters to the actual symbols they represent
57 | genericParams.expectKind(nnkGenericParams)
58 | for i, value in values:
59 | genericParams[i].expectKind(nnkSym)
60 | result[genericParams[i].strVal] = value
61 |
62 | proc replaceGenerics(typeDecl: NimNode, symLookup: Table[string, NimNode]): NimNode =
63 | ## Copies an AST, but replaces any generic references based on the given lookup table
64 | if typeDecl.kind in {nnkSym, nnkIdent} and typeDecl.strVal in symLookup:
65 | return symLookup[typeDecl.strVal]
66 | elif typeDecl.len == 0:
67 | return typeDecl
68 | result = newNimNode(typeDecl.kind)
69 | for child in typeDecl.children:
70 | result.add(child.replaceGenerics(symLookup))
71 |
72 | proc resolveBracketGeneric*(typeDef: NimNode): NimNode =
73 | ## Replaces a generic alias with the underlying type it represents
74 | typeDef.expectKind({nnkBracketExpr})
75 | let declaration = typeDef[0].getImpl
76 | declaration.expectKind(nnkTypeDef)
77 | let genericTable = declaration[1].asGenericTable(typeDef[1 ..^ 1])
78 | return declaration[2].replaceGenerics(genericTable)
79 |
80 | proc resolveTo*(typeDef: NimNode, expectKind: set[NimNodeKind]): Option[NimNode] =
81 | ## Resolves the system parsable type of an identifier
82 | if typeDef.kind in expectKind:
83 | return some(typeDef)
84 |
85 | case typeDef.kind
86 | of nnkBracketExpr:
87 | return typeDef.resolveBracketGeneric().resolveTo(expectKind)
88 | of nnkSym:
89 | return typeDef.getImpl.resolveTo(expectKind)
90 | of nnkTypeDef:
91 | return typeDef[2].resolveTo(expectKind)
92 | else:
93 | return none[NimNode]()
94 |
95 | proc resolveAlias*(typeDef: NimNode): Option[NimNode] =
96 | ## Attempts to resolve any aliases until a concrete type is reached
97 | case typeDef.kind
98 | of nnkSym:
99 | let impl = typeDef.getImpl
100 | if impl.kind == nnkTypeDef:
101 | return some(impl[2])
102 | of nnkBracketExpr:
103 | return some(typeDef.resolveBracketGeneric())
104 | else:
105 | discard
106 |
107 | proc findSym*(node: NimNode): NimNode =
108 | ## Unwraps the symbol from a node
109 | case node.kind
110 | of nnkSym:
111 | return node
112 | of nnkIdentDefs, nnkPragmaExpr:
113 | return node[0].findSym
114 | else:
115 | error("Could not extract a symbol from " & node.lispRepr, node)
116 |
--------------------------------------------------------------------------------
/tests/bundle_include.nim:
--------------------------------------------------------------------------------
1 | import necsus, unittest, sequtils
2 |
3 | type
4 | A* = object
5 | B* = object
6 | C* = object
7 |
8 | Grouping* = object
9 | create: FullSpawn[(A, B)]
10 | attach*: Attach[(C,)]
11 |
12 | proc setup*(bundle: Bundle[Grouping]) =
13 | let eid = bundle.create.with(A(), B())
14 | bundle.attach.exec(eid, (C(),))
15 |
16 | proc loop*(bundle: Bundle[Grouping], query: Query[(A, B, C)]) =
17 | setup(bundle)
18 | check(toSeq(query.items).len == 2)
19 |
20 | proc teardown*(bundle: Bundle[Grouping], query: Query[(A, B, C)]) =
21 | setup(bundle)
22 | check(toSeq(query.items).len == 3)
23 |
--------------------------------------------------------------------------------
/tests/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/../src")
2 | switch("experimental", "callOperator")
3 |
--------------------------------------------------------------------------------
/tests/privateSystem.nim:
--------------------------------------------------------------------------------
1 | import necsus, unittest, sequtils
2 |
3 | proc creator(spawn: Spawn[(string,)]) =
4 | spawn.with("foo")
5 |
6 | proc assertion*(query: Query[(string,)]) {.depends(creator).} =
7 | check(query.toSeq == @[("foo",)])
8 |
--------------------------------------------------------------------------------
/tests/t_accessory.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest]
2 |
3 | type
4 | Person = object
5 |
6 | Name = object
7 | name*: string
8 |
9 | Age {.accessory.} = object
10 | age*: int
11 |
12 | proc setup(spawn: Spawn[(Age, Name, Person)], spawn2: Spawn[(Name, Person)]) =
13 | spawn.with(Age(age: 50), Name(name: "Jack"), Person())
14 | spawn.with(Age(age: 40), Name(name: "Jill"), Person())
15 | spawn2.with(Name(name: "John"), Person())
16 |
17 | proc assertion(
18 | people: Query[tuple[person: Person, name: Name]],
19 | ages: Query[tuple[age: Age]],
20 | all: Query[tuple[person: Person, name: Name, age: Age]],
21 | ) =
22 | check(toSeq(people.items).mapIt(it.name.name) == @["Jack", "Jill", "John"])
23 | check(toSeq(ages.items).mapIt(it.age.age) == @[50, 40])
24 | check(toSeq(all.items).mapIt(it.name.name) == @["Jack", "Jill"])
25 |
26 | proc runner(tick: proc(): void) =
27 | tick()
28 |
29 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
30 |
31 | test "System with accessory components":
32 | myApp()
33 |
--------------------------------------------------------------------------------
/tests/t_accessory_attach.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest]
2 |
3 | type
4 | Person = object
5 |
6 | Name = string
7 |
8 | Age {.accessory.} = int
9 |
10 | proc setup(spawn: FullSpawn[(Name, Person)], add: Attach[(Age,)]) =
11 | spawn.with("Jack", Person()).add((50,))
12 |
13 | proc assertion(all: Query[tuple[name: Name, age: Age]]) =
14 | check(toSeq(all.items) == @[("Jack", 50)])
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 |
19 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
20 |
21 | test "Attaching an accessory component":
22 | myApp()
23 |
--------------------------------------------------------------------------------
/tests/t_accessory_detach.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest]
2 |
3 | type
4 | Person = object
5 |
6 | Name = string
7 |
8 | Age {.accessory.} = int
9 |
10 | proc setup(spawn: FullSpawn[(Name, Person, Age)], detach: Detach[(Age,)]) =
11 | spawn.with("Jack", Person(), 50).detach()
12 | discard spawn.with("Jill", Person(), 60)
13 |
14 | proc assertion(all: Query[(Name,)], aged: Query[(Name, Age)]) =
15 | check(toSeq(all.items) == @[("Jack",), ("Jill",)])
16 | check(toSeq(aged.items) == @[("Jill", 60)])
17 |
18 | proc runner(tick: proc(): void) =
19 | tick()
20 |
21 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
22 |
23 | test "Attaching an accessory component":
24 | myApp()
25 |
--------------------------------------------------------------------------------
/tests/t_accessory_len.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[unittest, options]
2 |
3 | type
4 | Person = object
5 |
6 | Name = object
7 | name*: string
8 |
9 | Age {.accessory.} = object
10 | age*: int
11 |
12 | proc setup(spawn: Spawn[(Age, Name, Person)], spawn2: Spawn[(Name, Person)]) =
13 | spawn.with(Age(age: 50), Name(name: "Jack"), Person())
14 | spawn.with(Age(age: 40), Name(name: "Jill"), Person())
15 | spawn2.with(Name(name: "John"), Person())
16 |
17 | proc assertion(
18 | people: Query[(Person, Name)],
19 | ages: Query[(Age,)],
20 | all: Query[(Person, Name, Age)],
21 | notAge: Query[(Person, Not[Age])],
22 | maybeAge: Query[(Person, Option[Age])],
23 | ) =
24 | check(people.len == 3)
25 | check(ages.len == 2)
26 | check(all.len == 2)
27 | check(notAge.len == 1)
28 | check(maybeAge.len == 3)
29 |
30 | proc runner(tick: proc(): void) =
31 | tick()
32 |
33 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
34 |
35 | test "Query length with accessory components":
36 | myApp()
37 |
--------------------------------------------------------------------------------
/tests/t_accessory_lookup.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[unittest, options]
2 |
3 | type
4 | Person = object
5 |
6 | Name {.accessory.} = string
7 |
8 | Age {.accessory.} = int
9 |
10 | proc exec(
11 | spawn1: FullSpawn[(Person,)],
12 | spawn2: FullSpawn[(Person, Name)],
13 | spawn3: FullSpawn[(Person, Name, Age)],
14 | lookup1: Lookup[(Name, Not[Age])],
15 | lookup2: Lookup[(Name, Age)],
16 | lookup3: Lookup[(Name, Option[ptr Age])],
17 | ) =
18 | let first = spawn1.with(Person())
19 | check(first.lookup1().isNone())
20 | check(first.lookup2().isNone())
21 | check(first.lookup3().isNone())
22 |
23 | let second = spawn2.with(Person(), "Jack")
24 | check(second.lookup1().get()[0] == "Jack")
25 | check(second.lookup2().isNone())
26 | check(second.lookup3() == some(("Jack", none(ptr Age))))
27 |
28 | let third = spawn3.with(Person(), "Jack", 25)
29 | check(third.lookup1().isNone())
30 | check(third.lookup2() == some(("Jack", 25)))
31 | check(third.lookup3().isSome())
32 |
33 | proc runner(tick: proc(): void) =
34 | tick()
35 |
36 | proc myApp() {.necsus(runner, [~exec], conf = newNecsusConf()).}
37 |
38 | test "Looking up an entity with an accessory":
39 | myApp()
40 |
--------------------------------------------------------------------------------
/tests/t_accessory_not.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest]
2 |
3 | type
4 | Person = object
5 |
6 | Name = string
7 |
8 | Age {.accessory.} = int
9 |
10 | Marbles {.accessory.} = int
11 |
12 | Arms = int
13 |
14 | proc setup(
15 | spawn1: Spawn[(Age, Name, Person)],
16 | spawn2: Spawn[(Marbles, Name, Person)],
17 | spawn3: Spawn[(Arms, Name, Person)],
18 | ) =
19 | spawn1.with(100, "Jack", Person())
20 | spawn2.with(41, "Jill", Person())
21 | spawn3.with(2, "John", Person())
22 |
23 | proc assertion(
24 | all: Query[(Name,)],
25 | notAged: Query[(Name, Not[Age])],
26 | noMarbles: Query[(Name, Not[Marbles])],
27 | marblesNoAge: Query[(Marbles, Not[Age])],
28 | ageNoMarbles: Query[(Age, Not[Marbles])],
29 | ) =
30 | check(toSeq(all.items) == @[("John",), ("Jack",), ("Jill",)])
31 | check(toSeq(notAged.items).mapIt(it[0]) == @["John", "Jill"])
32 | check(toSeq(noMarbles.items).mapIt(it[0]) == @["John", "Jack"])
33 | check(toSeq(marblesNoAge.items).mapIt(it[0]) == @[41])
34 | check(toSeq(ageNoMarbles.items).mapIt(it[0]) == @[100])
35 |
36 | proc runner(tick: proc(): void) =
37 | tick()
38 |
39 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
40 |
41 | test "Using a 'Not' query on accessory components":
42 | myApp()
43 |
--------------------------------------------------------------------------------
/tests/t_accessory_optional.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest, options]
2 |
3 | type
4 | Person = object
5 |
6 | Name = string
7 |
8 | Age {.accessory.} = int
9 |
10 | Marbles {.accessory.} = int
11 |
12 | proc setup(spawn1: Spawn[(Age, Name, Person)], spawn2: Spawn[(Marbles, Name, Person)]) =
13 | spawn1.with(100, "Jack", Person())
14 | spawn2.with(41, "Jill", Person())
15 |
16 | proc assertion(all: Query[(Name, Option[Age], Option[Marbles])]) =
17 | check(
18 | toSeq(all.items) ==
19 | @[("Jack", some(100), none(Marbles)), ("Jill", none(Age), some(41))]
20 | )
21 |
22 | proc runner(tick: proc(): void) =
23 | tick()
24 |
25 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
26 |
27 | test "Using an 'Optional' query on accessory components":
28 | myApp()
29 |
--------------------------------------------------------------------------------
/tests/t_accessory_optional_detach.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest, options]
2 |
3 | type
4 | Person = object
5 |
6 | Name = string
7 |
8 | Age {.accessory.} = int
9 |
10 | Unrelated = object
11 |
12 | proc setup(
13 | spawnWithAge: FullSpawn[(Name, Person, Age)],
14 | spawnNoAge: FullSpawn[(Name, Person)],
15 | detach: Detach[(Option[Age],)],
16 | spawnUnrelated: FullSpawn[(Unrelated,)],
17 | ) =
18 | spawnWithAge.with("Jack", Person(), 50).detach()
19 | spawnNoAge.with("Jill", Person()).detach()
20 |
21 | spawnUnrelated.with(Unrelated()).detach()
22 |
23 | proc assertion(noAge: Query[(Name, Not[Age])], aged: Query[(Age,)]) =
24 | check(noAge.mapIt(it[0]) == @["Jack", "Jill"])
25 | check(aged.len == 0)
26 |
27 | proc runner(tick: proc(): void) =
28 | tick()
29 |
30 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
31 |
32 | test "Optionally detaching an accessory component":
33 | myApp()
34 |
--------------------------------------------------------------------------------
/tests/t_accessory_optionptr.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest, options]
2 |
3 | type
4 | Name = string
5 |
6 | Age {.accessory.} = object
7 | age: int
8 |
9 | proc setup(spawn: FullSpawn[(Age, Name)], getAge: Lookup[(Option[ptr Age],)]) =
10 | spawn.with(Age(age: 41), "Jack").getAge().get()[0].get().age += 1
11 |
12 | proc assertion(all: Query[(Name, Age)]) =
13 | check(toSeq(all.items).mapIt(it[1].age) == @[42])
14 |
15 | proc runner(tick: proc(): void) =
16 | tick()
17 |
18 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
19 |
20 | test "Optional lookup with a pointer to an accessory":
21 | myApp()
22 |
--------------------------------------------------------------------------------
/tests/t_accessory_pragma.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest, sets]
2 |
3 | type
4 | Name = string
5 | Age {.byref, used, accessory.} = int
6 |
7 | proc setup(spawn1: Spawn[(Age, Name)], spawn2: Spawn[(Name,)]) =
8 | spawn1.with(41, "Jack")
9 | spawn2.with("Jill")
10 |
11 | proc assertion(people: Query[(Name,)], ages: Query[(Name, Age)]) =
12 | check(toSeq(people.items).toHashSet == [("Jack",), ("Jill",)].toHashSet)
13 | check(toSeq(ages.items) == @[("Jack", 41)])
14 |
15 | proc runner(tick: proc(): void) =
16 | tick()
17 |
18 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
19 |
20 | test "System with accessory pragmas alongside other pragmas":
21 | myApp()
22 |
--------------------------------------------------------------------------------
/tests/t_accessory_swap.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[unittest, sequtils, sets]
2 |
3 | type
4 | Person = object
5 |
6 | Name = string
7 |
8 | Immortal = bool
9 |
10 | Age {.accessory.} = int
11 |
12 | LostTheirMarbles = object
13 |
14 | Marbles {.accessory.} = int
15 |
16 | proc setup(
17 | spawn: FullSpawn[(Age, LostTheirMarbles, Name, Person)],
18 | markImmortal: Swap[(Immortal,), (Age,)],
19 | giveMarbles: Swap[(Marbles,), (LostTheirMarbles,)],
20 | ) =
21 | discard spawn.with(41, LostTheirMarbles(), "John", Person())
22 | spawn.with(50, LostTheirMarbles(), "Jack", Person()).markImmortal((true,))
23 | spawn.with(30, LostTheirMarbles(), "Jane", Person()).giveMarbles((5,))
24 |
25 | proc assertion(
26 | all: Query[(Name,)], aged: Query[(Name, Age)], marbles: Query[(Name, Marbles)]
27 | ) =
28 | check(toSeq(all.items).mapIt(it[0]).toHashSet == @["Jack", "John", "Jane"].toHashSet)
29 | check(toSeq(aged.items) == @[("Jane", 30), ("John", 41)])
30 | check(toSeq(marbles.items) == @[("Jane", 5)])
31 |
32 | proc runner(tick: proc(): void) =
33 | tick()
34 |
35 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
36 |
37 | test "Swapping an accessory component":
38 | myApp()
39 |
--------------------------------------------------------------------------------
/tests/t_aliasWithCompGeneric.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type Components[T] = (T, string)
4 |
5 | proc spawner(spawnInt: Spawn[Components[int]], spawnFloat: Spawn[Components[float]]) =
6 | spawnInt.with(123, "foo")
7 | spawnFloat.with(3.14, "bar")
8 |
9 | proc verify(queryInt: Query[Components[int]], queryFloat: Query[Components[float]]) =
10 | check(queryInt.toSeq == @[(123, "foo")])
11 | check(queryFloat.toSeq == @[(3.14, "bar")])
12 |
13 | proc runner(tick: proc(): void) =
14 | tick()
15 |
16 | proc myApp() {.necsus(runner, [~spawner, ~verify], conf = newNecsusConf()).}
17 |
18 | test "Directives with generics for component list":
19 | myApp()
20 |
--------------------------------------------------------------------------------
/tests/t_aliasWithGenerics.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | BaseSpawn[T] = Spawn[(T, string)]
5 |
6 | BaseQuery[T] = Query[(T, string)]
7 |
8 | proc spawner(spawnInt: BaseSpawn[int], spawnFloat: BaseSpawn[float]) =
9 | spawnInt.with(123, "foo")
10 | spawnFloat.with(3.14, "bar")
11 |
12 | proc verify(queryInt: BaseQuery[int], queryFloat: BaseQuery[float]) =
13 | check(queryInt.toSeq == @[(123, "foo")])
14 | check(queryFloat.toSeq == @[(3.14, "bar")])
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 |
19 | proc myApp() {.necsus(runner, [~spawner, ~verify], conf = newNecsusConf()).}
20 |
21 | test "Directives aliase with generic parameters":
22 | myApp()
23 |
--------------------------------------------------------------------------------
/tests/t_aliases.nim:
--------------------------------------------------------------------------------
1 | import unittest, sequtils, necsus
2 |
3 | type
4 | Thingy = object
5 | value: string
6 |
7 | Whatsit = Thingy
8 | Whosit = Thingy
9 | Whysit = Thingy
10 | Whensit = Thingy
11 |
12 | ParametricAlias[T] = proc(spawn: Spawn[(T,)]): void
13 | ExactAlias = proc(spawn: Spawn[(Whysit,)]): void
14 | AliasAlias = ParametricAlias[Whensit]
15 |
16 | proc spawner[T](value: string): proc(spawn: Spawn[(T,)]): void =
17 | return proc(spawn: Spawn[(T,)]) =
18 | spawn.with(T(value: value))
19 |
20 | let spawn1 = spawner[Thingy]("first")
21 | let spawn2: proc(spawn: Spawn[(Whatsit,)]): void = spawner[Whatsit]("second")
22 | let spawn3: ParametricAlias[Whosit] = spawner[Whosit]("third")
23 | let spawn4: ExactAlias = spawner[Whysit]("fourth")
24 | let spawn5: AliasAlias = spawner[Whensit]("fifth")
25 |
26 | proc assertion(
27 | thingies: Query[(Thingy,)],
28 | whatsits: Query[(Whatsit,)],
29 | whosits: Query[(Whosit,)],
30 | whysit: Query[(Whysit,)],
31 | whensit: Query[(Whensit,)],
32 | ) =
33 | check(toSeq(thingies.items).mapIt(it[0].value) == @["first"])
34 | check(toSeq(whatsits.items).mapIt(it[0].value) == @["second"])
35 | check(toSeq(whosits.items).mapIt(it[0].value) == @["third"])
36 | check(toSeq(whysit.items).mapIt(it[0].value) == @["fourth"])
37 | check(toSeq(whensit.items).mapIt(it[0].value) == @["fifth"])
38 |
39 | proc runner(tick: proc(): void) =
40 | tick()
41 |
42 | proc myApp() {.
43 | necsus(
44 | runner,
45 | [~spawn1, ~spawn2, ~spawn3, ~spawn4, ~spawn5, ~assertion],
46 | conf = newNecsusConf(),
47 | )
48 | .}
49 |
50 | test "Spawning against aliased types should remain distinct":
51 | myApp()
52 |
--------------------------------------------------------------------------------
/tests/t_appReturn.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, options
2 |
3 | proc setReturnValue(returnValue: Shared[string]) =
4 | returnValue.set("foobar")
5 |
6 | proc runner(tick: proc(): void) =
7 | tick()
8 |
9 | proc appReturnValue(): string {.necsus(runner, [~setReturnValue], newNecsusConf()).}
10 |
11 | test "Use shared values for app return values":
12 | check(appReturnValue() == "foobar")
13 |
14 | test "Fail if the return value isn't provided by a shared variable":
15 | when compiles(
16 | proc() =
17 | proc noAppReturnValue(): string {.necsus(runner, [], newNecsusConf()).}
18 | ):
19 | fail()
20 |
21 | proc declaresReturnValue(returnValue: Shared[string]) =
22 | discard
23 |
24 | proc unsetAppReturnValue(): string {.
25 | necsus(runner, [~declaresReturnValue], newNecsusConf())
26 | .}
27 |
28 | test "Throw if the return value is never set":
29 | expect UnpackDefect:
30 | discard unsetAppReturnValue()
31 |
--------------------------------------------------------------------------------
/tests/t_attach.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | Name = object
5 | name*: string
6 |
7 | Age = object
8 | age*: int
9 |
10 | FavoriteNumber = object
11 | number*: int
12 |
13 | proc setup(spawn: Spawn[(Name,)]) =
14 | spawn.with(Name(name: "Foo"))
15 | spawn.with(Name(name: "Bar"))
16 |
17 | proc modify(
18 | all: FullQuery[(Name,)], addAge: Attach[(Age,)], addNum: Attach[(FavoriteNumber,)]
19 | ) =
20 | var i = 0
21 | for entityId, _ in all:
22 | i += 1
23 | entityId.addAge((Age(age: i + 20),))
24 | entityId.addNum((FavoriteNumber(number: i),))
25 |
26 | proc assertions(all: Query[(Name, Age, FavoriteNumber)]) =
27 | check(toSeq(all.items).mapIt(it[0].name) == @["Foo", "Bar"])
28 | check(toSeq(all.items).mapIt(it[1].age) == @[21, 22])
29 | check(toSeq(all.items).mapIt(it[2].number) == @[1, 2])
30 |
31 | proc runner(tick: proc(): void) =
32 | tick()
33 |
34 | proc testAttaches() {.necsus(runner, [~setup, ~modify, ~assertions], newNecsusConf()).}
35 |
36 | test "Attaching components":
37 | testAttaches()
38 |
--------------------------------------------------------------------------------
/tests/t_attachFiltering.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | Name = object
5 | name*: string
6 |
7 | Age = object
8 | age*: int
9 |
10 | FavoriteNumber = object
11 | number*: int
12 |
13 | proc setup(spawn: Spawn[(Name,)], number: Spawn[(FavoriteNumber,)]) {.startupSys.} =
14 | spawn.with(Name(name: "Foo"))
15 |
16 | proc modify(all: FullQuery[(Name,)], addAge: Attach[(Age,)]) =
17 | for entityId, _ in all:
18 | entityId.addAge((Age(age: 20),))
19 |
20 | proc assertions(all: Query[(Name, Age)]) =
21 | check(toSeq(all.items).mapIt(it[0].name) == @["Foo"])
22 | check(toSeq(all.items).mapIt(it[1].age) == @[20])
23 |
24 | proc runner(tick: proc(): void) =
25 | tick()
26 |
27 | proc testAttaches() {.necsus(runner, [~setup, ~modify, ~assertions], newNecsusConf()).}
28 |
29 | test "Attaching components":
30 | testAttaches()
31 |
--------------------------------------------------------------------------------
/tests/t_attach_disjoint.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | Name = string
5 | Age = int
6 | Stunned {.accessory.} = object
7 |
8 | Item = object
9 | Title = string
10 | Broken {.accessory.} = object
11 |
12 | proc setup(person: Spawn[(Age, Name)], inventory: Spawn[(Item, Title)]) =
13 | person.with(31, "Jack")
14 | inventory.with(Item(), "Sword")
15 | inventory.with(Item(), "Dagger")
16 |
17 | proc breakItems(inventory: FullQuery[(Item,)], broken: Attach[(Broken,)]) =
18 | for entityId, _ in inventory:
19 | entityId.broken((Broken(),))
20 |
21 | proc stunPeople(people: FullQuery[(Name,)], stun: Attach[(Stunned,)]) =
22 | for entityId, _ in people:
23 | entityId.stun((Stunned(),))
24 |
25 | proc assertions(broken: Query[(Broken, Title)], stunned: Query[(Stunned, Name)]) =
26 | check(toSeq(broken.items).mapIt(it[1]) == @["Sword", "Dagger"])
27 | check(toSeq(stunned.items).mapIt(it[1]) == @["Jack"])
28 |
29 | proc runner(tick: proc(): void) =
30 | tick()
31 |
32 | proc testAttaches() {.
33 | necsus(runner, [~setup, ~breakItems, ~stunPeople, ~assertions], newNecsusConf())
34 | .}
35 |
36 | test "Attach with disjoin archetypes present":
37 | testAttaches()
38 |
--------------------------------------------------------------------------------
/tests/t_basic.nim:
--------------------------------------------------------------------------------
1 | import unittest, sequtils, necsus, sets
2 |
3 | type
4 | Person = object
5 | Name = object
6 | name*: string
7 |
8 | Age = object
9 | age*: int
10 |
11 | proc setup1(spawn: Spawn[(Name, Person)], spawnAll: Spawn[(Age, Name, Person)]) =
12 | spawn.with(Name(name: "Jack"), Person())
13 | spawn.with(Name(name: "Jill"), Person())
14 | spawnAll.with(Age(age: 40), Name(name: "John"), Person())
15 |
16 | proc setup2(spawnAge: Spawn[(Age,)], spawnPerson: Spawn[(Person,)]) =
17 | spawnAge.with(Age(age: 39))
18 | spawnPerson.with(Person())
19 | spawnPerson.with(Person())
20 |
21 | proc spawnMore(spawn: Spawn[(Name, Person)]) =
22 | spawn.with(Name(name: "Joe"), Person())
23 |
24 | proc assertion(
25 | people: Query[tuple[person: Person, name: Name]],
26 | ages: Query[tuple[age: Age]],
27 | all: Query[tuple[person: Person, name: Name, age: Age]],
28 | ) =
29 | check(
30 | toSeq(people.items).mapIt(it.name.name).toHashSet() ==
31 | ["Jack", "Jill", "Joe", "John"].toHashSet()
32 | )
33 | check(toSeq(ages.items).mapIt(it.age.age).toHashSet == [40, 39].toHashSet())
34 | check(toSeq(all.items).mapIt(it.name.name) == @["John"])
35 |
36 | proc runner(tick: proc(): void) =
37 | tick()
38 |
39 | proc myApp() {.
40 | necsus(runner, [~setup1, ~setup2, ~spawnMore, ~assertion], conf = newNecsusConf())
41 | .}
42 |
43 | test "Basic system":
44 | myApp()
45 |
--------------------------------------------------------------------------------
/tests/t_bits.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus/util/bits, sequtils, sets
2 |
3 | suite "Bits":
4 | test "Bit cardinality":
5 | var bits = Bits()
6 | check(bits.card == 0)
7 |
8 | bits.incl(4)
9 | check(bits.card == 1)
10 |
11 | bits.incl(500)
12 | check(bits.card == 2)
13 |
14 | bits.incl(500)
15 | check(bits.card == 2)
16 |
17 | test "Bit equality":
18 | var bits1 = Bits()
19 | var bits2 = Bits()
20 |
21 | check(bits1 == bits2)
22 | check(bits2 == bits1)
23 |
24 | bits1.incl(1)
25 | check(bits1 != bits2)
26 | check(bits2 != bits1)
27 |
28 | bits2.incl(1)
29 | check(bits1 == bits2)
30 | check(bits2 == bits1)
31 |
32 | bits1.incl(500)
33 | check(bits1 != bits2)
34 | check(bits2 != bits1)
35 |
36 | bits2.incl(500)
37 | check(bits1 == bits2)
38 | check(bits2 == bits1)
39 |
40 | test "Bit contins":
41 | let bits = newBits(1, 2, 3, 4, 500)
42 | check(1 in bits)
43 | check(4 in bits)
44 | check(500 in bits)
45 | check(5 notin bits)
46 | check(5000 notin bits)
47 |
48 | test "Bit addition":
49 | let bits1 = newBits(1)
50 | let bits2 = newBits(500)
51 |
52 | check(bits1 + bits2 == newBits(1, 500))
53 | check((bits1 + bits2).card == 2)
54 |
55 | test "In-place Bit addition":
56 | var bits = newBits(1, 5, 10, 500)
57 | bits += newBits(5, 6, 500, 700)
58 |
59 | check(bits == newBits(1, 5, 6, 10, 500, 700))
60 | check(bits.card == 6)
61 |
62 | test "Bit subtraction":
63 | check(newBits(1, 500) - newBits(500) == newBits(1))
64 | check(newBits(1, 500) - newBits(1) == newBits(500))
65 |
66 | test "Bit strict subset":
67 | let bits1 = newBits(1, 500)
68 | let bits2 = newBits(500)
69 | let bits3 = newBits(1)
70 |
71 | check(bits2 < bits1)
72 | check(bits3 < bits1)
73 | check(not (bits1 < bits2))
74 | check(not (bits1 < bits3))
75 | check(not (bits1 < bits1))
76 |
77 | test "Bit subset":
78 | let bits1 = newBits(1, 4)
79 | let bits2 = newBits(4)
80 | let bits3 = newBits(1)
81 |
82 | check(bits2 <= bits1)
83 | check(bits3 <= bits1)
84 | check(bits1 <= bits1)
85 | check(not (bits1 <= bits2))
86 | check(not (bits1 <= bits3))
87 | check(not (newBits(1, 500) <= newBits(1, 2, 3)))
88 | check(newBits(1, 2, 3) <= newBits(1, 2, 3, 500))
89 |
90 | check(not (bits2 > bits1))
91 | check(not (bits3 > bits1))
92 | check(not (bits1 > bits1))
93 | check(bits1 > bits2)
94 | check(bits1 > bits3)
95 |
96 | test "Bit anyIntersect":
97 | check(newBits(1, 2, 3).anyIntersect(newBits(3, 4, 5)))
98 | check(newBits(1, 200, 300).anyIntersect(newBits(300, 400, 500)))
99 | check(not newBits(1, 2, 3).anyIntersect(newBits(4, 5, 6)))
100 | check(not newBits(1, 200, 300).anyIntersect(newBits(400, 500, 600)))
101 |
102 | test "Bit iteration":
103 | var bits = Bits()
104 | check(bits.toSeq.len == 0)
105 |
106 | bits.incl(2)
107 | check(bits.toSeq == @[2'u16])
108 |
109 | bits.incl(200)
110 | check(bits.toSeq == @[2'u16, 200])
111 |
112 | test "Bit to string":
113 | var bits = Bits()
114 | check($bits == "{}")
115 |
116 | bits.incl(2)
117 | check($bits == "{2}")
118 |
119 | bits.incl(500)
120 | check($bits == "{2, 500}")
121 |
122 | test "Storing bits in sets":
123 | var storage = initHashSet[Bits]()
124 |
125 | check(newBits(1, 2, 3) notin storage)
126 |
127 | storage.incl(newBits(1, 2, 3))
128 | check(newBits(1, 2, 3) in storage)
129 | check(storage.len == 1)
130 |
131 | storage.incl(newBits(1, 2, 3))
132 | check(newBits(1, 2, 3) in storage)
133 | check(storage.len == 1)
134 |
135 | test "Filters":
136 | let filter = newFilter(mustContain = newBits(1, 5, 40), mustExclude = newBits(4))
137 |
138 | check(filter.matches(all = newBits(1, 5, 40)))
139 | check(filter.matches(all = newBits(1, 5, 40, 80, 100)))
140 | check(not filter.matches(all = newBits(1, 100, 400)))
141 | check(not filter.matches(all = newBits(1, 5)))
142 | check(not filter.matches(all = newBits(1, 4, 5, 40, 50)))
143 |
144 | check(
145 | newFilter(mustContain = newBits(), mustExclude = newBits(40)).matches(
146 | all = newBits(1, 2, 3)
147 | )
148 | )
149 |
150 | check(filter.matches(all = newBits(1, 5, 40), optional = newBits(5, 40)))
151 | check(filter.matches(all = newBits(1, 4, 5, 40), optional = newBits(4)))
152 |
--------------------------------------------------------------------------------
/tests/t_blockstore.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus/util/blockstore, sequtils
2 |
3 | suite "BlockStore":
4 | test "Pushing values":
5 | var store = newBlockStore[string](50)
6 | check(store.len == 0)
7 |
8 | let id1 = store.push("foo")
9 | check(store[id1] == "foo")
10 | check(store.items.toSeq == @["foo"])
11 | check(store.len == 1)
12 |
13 | let id2 = store.push("bar")
14 | check(store[id1] == "foo")
15 | check(store[id2] == "bar")
16 | check(store.items.toSeq == @["foo", "bar"])
17 | check(store.len == 2)
18 |
19 | let id3 = store.push("baz")
20 | check(store[id1] == "foo")
21 | check(store[id2] == "bar")
22 | check(store[id3] == "baz")
23 | check(store.items.toSeq == @["foo", "bar", "baz"])
24 | check(store.len == 3)
25 |
26 | test "Deleting values":
27 | var store = newBlockStore[int](50)
28 |
29 | let id0 = store.push(0)
30 | let id1 = store.push(1)
31 | let id2 = store.push(2)
32 | let id3 = store.push(3)
33 |
34 | check(store.items.toSeq == @[0, 1, 2, 3])
35 | check(store.len == 4)
36 |
37 | check(store.del(id2) == 2)
38 | check(store.items.toSeq == @[0, 1, 3])
39 | check(store.len == 3)
40 |
41 | check(store.del(id0) == 0)
42 | check(store.items.toSeq == @[1, 3])
43 | check(store.len == 2)
44 |
45 | check(store.del(id3) == 3)
46 | check(store.items.toSeq == @[1])
47 | check(store.len == 1)
48 |
49 | check(store.del(id1) == 1)
50 | check(store.items.toSeq == newSeq[int]())
51 | check(store.len == 0)
52 |
53 | test "Fail when indexes are out of bounds":
54 | var store = newBlockStore[string](5)
55 |
56 | for i in 1 .. 5:
57 | discard store.push("foo")
58 |
59 | expect IndexDefect:
60 | discard store.push("foo")
61 |
62 | expect IndexDefect:
63 | discard store.del(50)
64 |
65 | expect IndexDefect:
66 | discard store[50]
67 |
68 | test "Re-using deleted slots":
69 | var store = newBlockStore[int](10)
70 | for i in 0 .. 100:
71 | let idx = store.push(i)
72 | check(store[idx] == i)
73 | discard store.del(idx)
74 |
75 | test "Reserving values":
76 | var store = newBlockStore[string](50)
77 |
78 | var e1 = store.reserve
79 | check(e1.index == 0)
80 | e1.set("foo")
81 | check(store[e1.index] == "foo")
82 | check(store.items.toSeq == @["foo"])
83 |
84 | var e2 = store.reserve
85 | check(e2.index == 1)
86 | e2.value.add("bar")
87 | e2.commit
88 | check(store[e2.index] == "bar")
89 | check(store.items.toSeq == @["foo", "bar"])
90 |
91 | test "Manual iteration":
92 | var store = newBlockStore[string](50)
93 | let id1 = store.push("foo")
94 | let id2 = store.push("bar")
95 | let id3 = store.push("baz")
96 |
97 | var iter: BlockIter
98 | check(not iter.isDone)
99 | check(store.next(iter)[] == "foo")
100 | check(not iter.isDone)
101 | check(store.next(iter)[] == "bar")
102 | check(not iter.isDone)
103 | check(store.next(iter)[] == "baz")
104 | check(not iter.isDone)
105 | check(store.next(iter) == nil)
106 | check(iter.isDone)
107 |
--------------------------------------------------------------------------------
/tests/t_bundle.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, bundle_include
2 |
3 | proc runner(tick: proc(): void) =
4 | tick()
5 |
6 | proc myApp() {.necsus(runner, [~setup, ~loop, ~teardown], conf = newNecsusConf()).}
7 |
8 | test "Bundling directives into an object":
9 | myApp()
10 |
--------------------------------------------------------------------------------
/tests/t_bundleEmptyObject.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type A = object
4 |
5 | proc system(bundle: Bundle[A]) =
6 | discard
7 |
8 | proc runner(tick: proc(): void) =
9 | tick()
10 |
11 | proc myApp() {.necsus(runner, [~system], conf = newNecsusConf()).}
12 |
13 | test "Bundles that reference empty objects should compile":
14 | myApp()
15 |
--------------------------------------------------------------------------------
/tests/t_bundleFromBuiltSystem.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type Controller = object
4 | data: Shared[string]
5 |
6 | proc build(): auto =
7 | return proc(bundle: Bundle[Controller]) =
8 | bundle.data := "foo"
9 |
10 | let logic = build()
11 |
12 | proc assertion(bundle: Bundle[Controller]) =
13 | check(bundle.data == "foo")
14 |
15 | proc runner(tick: proc(): void) =
16 | tick()
17 |
18 | proc myApp() {.necsus(runner, [~logic, ~assertion], conf = newNecsusConf()).}
19 |
20 | test "Bundles used with a constructed system":
21 | myApp()
22 |
--------------------------------------------------------------------------------
/tests/t_bundleInstanced.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = string
5 |
6 | B = object
7 | a: Shared[A]
8 |
9 | proc logic(bundle: Bundle[B]): auto {.instanced.} =
10 | return proc() =
11 | bundle.a := "foo"
12 |
13 | proc assertion(bundle: Bundle[B]) =
14 | check(bundle.a == "foo")
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 |
19 | proc myApp() {.necsus(runner, [~logic, ~assertion], conf = newNecsusConf()).}
20 |
21 | test "Bundles used within an instanced system":
22 | myApp()
23 |
--------------------------------------------------------------------------------
/tests/t_bundleNested.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = object
5 | spawn: Spawn[(string,)]
6 | query: Query[(string,)]
7 |
8 | B = object
9 | a: Bundle[A]
10 |
11 | C = object
12 | b: Bundle[B]
13 |
14 | proc setup*(bundle: Bundle[C]) =
15 | discard
16 |
17 | proc assertion*(bundle: Bundle[C]) =
18 | discard
19 |
20 | proc runner(tick: proc(): void) =
21 | tick()
22 |
23 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
24 |
25 | test "Bundles nested inside other bundles":
26 | myApp()
27 |
--------------------------------------------------------------------------------
/tests/t_bundleWithGenerics.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type A[T] = object
4 | value: Shared[T]
5 |
6 | proc setValue(bundle: Bundle[A[string]]) =
7 | bundle.value := "foo"
8 |
9 | proc verify(bundle: Bundle[A[string]]) =
10 | check(bundle.value.getOrRaise == "foo")
11 |
12 | proc runner(tick: proc(): void) =
13 | tick()
14 |
15 | proc myApp() {.necsus(runner, [~setValue, ~verify], conf = newNecsusConf()).}
16 |
17 | test "Bundles with generic parameters should compile":
18 | myApp()
19 |
--------------------------------------------------------------------------------
/tests/t_bundleWithInbox.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type A = object
4 | events: Inbox[string]
5 |
6 | proc setup*(send: Outbox[string]) =
7 | send("foo")
8 |
9 | proc assertion1*(bundle: Bundle[A]) =
10 | check(bundle.events.toSeq == @["foo"])
11 |
12 | proc assertion2*(bundle: Bundle[A]) =
13 | check(bundle.events.len == 0)
14 |
15 | proc runner(tick: proc(): void) =
16 | tick()
17 | tick()
18 | tick()
19 |
20 | proc myApp() {.
21 | necsus(runner, [~setup, ~assertion1, ~assertion2], conf = newNecsusConf())
22 | .}
23 |
24 | test "Bundles that contain an inbox":
25 | myApp()
26 |
--------------------------------------------------------------------------------
/tests/t_bundleWithLocal.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = object
5 | foo: Local[string]
6 | bar: Local[string]
7 |
8 | B = object
9 | foo: Local[string]
10 | bar: Local[string]
11 |
12 | C[T] = object
13 | data: Local[seq[T]]
14 |
15 | proc assertion1*(
16 | a: Bundle[A], b: Bundle[B], c1: Bundle[C[string]], c2: Bundle[C[string]]
17 | ) =
18 | a.foo := "foo"
19 | a.bar := "bar"
20 | b.foo := "baz"
21 | b.bar := "qux"
22 |
23 | c1.data := @["wakka"]
24 |
25 | proc assertion2*(
26 | a: Bundle[A], b: Bundle[B], c1: Bundle[C[string]], c2: Bundle[C[string]]
27 | ) =
28 | check(a.foo == "foo")
29 | check(a.bar == "bar")
30 | check(b.foo == "baz")
31 | check(b.bar == "qux")
32 |
33 | check(c1.data == @["wakka"])
34 | check(c2.data == @["wakka"])
35 |
36 | proc runner(tick: proc(): void) =
37 | tick()
38 |
39 | proc myApp() {.necsus(runner, [~assertion1, ~assertion2], conf = newNecsusConf()).}
40 |
41 | test "Bundles that contain Locals":
42 | myApp()
43 |
--------------------------------------------------------------------------------
/tests/t_bundleWithTimes.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, os
2 |
3 | type A = object
4 | delta: TimeDelta
5 | elapsed: TimeElapsed
6 |
7 | var lastElapsed = -1.0
8 | var lastDelta = -1.0
9 | var timesThrough = 1
10 |
11 | proc assertion*(bundle: Bundle[A]) =
12 | check(bundle.elapsed() > lastElapsed)
13 | check(bundle.delta() > lastDelta)
14 |
15 | lastElapsed = bundle.elapsed()
16 |
17 | sleep(timesThrough * 10)
18 | timesThrough += 1
19 |
20 | proc runner(tick: proc(): void) =
21 | tick()
22 | tick()
23 | tick()
24 |
25 | proc myApp() {.necsus(runner, [~assertion], conf = newNecsusConf()).}
26 |
27 | test "Bundles that contain time references":
28 | myApp()
29 |
--------------------------------------------------------------------------------
/tests/t_componentTuple.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | proc spawner(
4 | spawn: Spawn[tuple[value: tuple[nested: string]]],
5 | global: Shared[tuple[value: string]],
6 | ) =
7 | spawn.with(("foo",))
8 | global := ("blah",)
9 |
10 | proc assertion(
11 | query: Query[tuple[value: tuple[nested: string]]],
12 | global: Shared[tuple[value: string]],
13 | ) =
14 | check(query.toSeq == @[(("foo",),)])
15 | check(global == ("blah",))
16 |
17 | proc runner(tick: proc(): void) =
18 | tick()
19 |
20 | proc myApp() {.necsus(runner, [~spawner, ~assertion], newNecsusConf()).}
21 |
22 | test "Systems should allow tuples as components":
23 | myApp()
24 |
--------------------------------------------------------------------------------
/tests/t_defaultRunner.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc system(iterations: Local[int], exit: Shared[NecsusRun]) =
4 | if iterations.get(0) >= 10:
5 | exit := ExitLoop
6 | else:
7 | iterations := iterations.get(0) + 1
8 |
9 | proc testDefaultGampeLoop() {.necsus(gameLoop, [~system], newNecsusConf()).}
10 |
11 | test "Default game loop runner":
12 | testDefaultGampeLoop()
13 |
--------------------------------------------------------------------------------
/tests/t_delete.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type Thingy = object
4 | number: int
5 |
6 | proc setup(spawn: Spawn[(Thingy,)]) =
7 | for i in 1 .. 10:
8 | spawn.with(Thingy(number: i))
9 |
10 | proc rm(all: FullQuery[tuple[thingy: Thingy]], delete: Delete) =
11 | for entityId, info in all:
12 | if info.thingy.number mod 2 == 0:
13 | delete(entityId)
14 |
15 | proc assertions(all: Query[(Thingy,)]) =
16 | check(toSeq(all.items).mapIt(it[0].number) == @[1, 3, 5, 7, 9])
17 |
18 | proc runner(tick: proc(): void) =
19 | tick()
20 |
21 | proc myApp() {.necsus(runner, [~setup, ~rm, ~assertions], newNecsusConf()).}
22 |
23 | test "Deleting entities":
24 | myApp()
25 |
--------------------------------------------------------------------------------
/tests/t_deleteAll.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | Thingy = int
5 |
6 | Excluded = object
7 |
8 | proc setup(spawn: Spawn[(Thingy,)], spawn2: Spawn[(Thingy, Excluded)]) =
9 | for i in 1 .. 10:
10 | spawn.with(i)
11 |
12 | for i in 1 .. 10:
13 | spawn2.with(i, Excluded())
14 |
15 | proc rm(del: DeleteAll[(Thingy, Not[Excluded])]) =
16 | del()
17 |
18 | proc assertions(all: Query[(Thingy,)]) =
19 | check(all.len == 10)
20 |
21 | proc runner(tick: proc(): void) =
22 | tick()
23 |
24 | proc myApp() {.necsus(runner, [~setup, ~rm, ~assertions], newNecsusConf()).}
25 |
26 | test "Deleting all entities":
27 | myApp()
28 |
--------------------------------------------------------------------------------
/tests/t_deleteCallsDestroy.nim:
--------------------------------------------------------------------------------
1 | import necsus/util/tools
2 |
3 | when isSinkMemoryCorruptionFixed():
4 | import unittest, necsus
5 |
6 | type Thingy = object
7 | value: int
8 |
9 | proc `=copy`(a: var Thingy, b: Thingy) {.error.}
10 |
11 | var thingyDestroyCount = 0
12 |
13 | {.warning[Deprecated]: off.}
14 | proc `=destroy`(thingy: var Thingy) =
15 | if thingy.value == 123:
16 | assert(thingyDestroyCount <= 0)
17 | thingyDestroyCount += 1
18 |
19 | proc destroyObj(spawn: FullSpawn[(Thingy,)], delete: Delete) =
20 | require(thingyDestroyCount == 0)
21 | let eid = spawn.with(Thingy(value: 123))
22 | require(thingyDestroyCount == 0)
23 | delete(eid)
24 | require(thingyDestroyCount == 1)
25 |
26 | proc runner(tick: proc(): void) =
27 | tick()
28 |
29 | proc myApp() {.necsus(runner, [~destroyObj], newNecsusConf()).}
30 |
31 | test "Deleting entities should call destroy on their components":
32 | myApp()
33 |
--------------------------------------------------------------------------------
/tests/t_depends.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type Accum = object
4 | data: string
5 |
6 | proc creator(spawn: Spawn[(Accum,)]) =
7 | spawn.with(Accum(data: "create"))
8 |
9 | proc buildSystem(): auto =
10 | return proc(query: Query[(ptr Accum,)]) =
11 | for (elem) in query:
12 | elem.data &= " update"
13 |
14 | let update {.depends(creator), used.} = buildSystem()
15 |
16 | proc update2(query: Query[(ptr Accum,)]) {.depends(update).} =
17 | for (elem) in query:
18 | elem.data &= " another"
19 |
20 | proc update3(query: Query[(ptr Accum,)]) {.depends(update2).} =
21 | for (elem) in query:
22 | elem.data &= " also"
23 |
24 | proc assertion(query: Query[(Accum,)]) {.depends(update2, update3).} =
25 | check(query.len == 1)
26 | for (elem) in query:
27 | check(elem.data == "create update another also")
28 |
29 | proc runner(tick: proc(): void) =
30 | tick()
31 |
32 | proc myApp() {.necsus(runner, [~assertion], newNecsusConf()).}
33 |
34 | test "Depending on other systems":
35 | myApp()
36 |
--------------------------------------------------------------------------------
/tests/t_dependsOnPrivate.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, privateSystem
2 |
3 | proc runner(tick: proc(): void) =
4 | tick()
5 |
6 | proc myApp() {.necsus(runner, [~assertion], newNecsusConf()).}
7 |
8 | test "Depending on other systems with private visibility":
9 | myApp()
10 |
--------------------------------------------------------------------------------
/tests/t_detach.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | A = object
5 | value: int
6 |
7 | B = object
8 | value: int
9 |
10 | C = object
11 | value: int
12 |
13 | proc setup(spawn: Spawn[(A, B, C)]) =
14 | for i in 1 .. 10:
15 | spawn.with(A(value: i), B(value: i), C(value: i))
16 |
17 | proc detacher(
18 | abc: FullQuery[tuple[a: A, b: B, c: C]],
19 | detachBC: Detach[(B, C)],
20 | detachC: Detach[(C,)],
21 | ) =
22 | for eid, comps in abc:
23 | if comps.a.value <= 3:
24 | detachBC(eid)
25 | elif comps.a.value <= 6:
26 | detachC(eid)
27 |
28 | proc assertDetached(abc: Query[(A, B, C)], ab: Query[(A, B)], a: Query[(A,)]) =
29 | check(toSeq(abc.items).len == 4)
30 | check(toSeq(ab.items).len == 7)
31 | check(toSeq(a.items).len == 10)
32 |
33 | proc reattach(query: FullQuery[(A,)], attach: Attach[(B, C)]) =
34 | for eid, _ in query:
35 | eid.attach((B(value: 1), C(value: 1)))
36 |
37 | proc assertReattached(abc: Query[(A, B, C)]) =
38 | check(toSeq(abc.items).len == 10)
39 |
40 | proc runner(tick: proc(): void) =
41 | tick()
42 |
43 | proc testDetach() {.
44 | necsus(
45 | runner,
46 | [~setup, ~detacher, ~assertDetached, ~reattach, ~assertReattached],
47 | newNecsusConf(),
48 | )
49 | .}
50 |
51 | test "Detaching components":
52 | testDetach()
53 |
--------------------------------------------------------------------------------
/tests/t_detachOptional.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils, options
2 |
3 | type
4 | A = char
5 | B = char
6 | C = char
7 | D = char
8 | E = char
9 |
10 | proc setup(spawnABCD: Spawn[(A, B, C, D)], spawnABCDE: Spawn[(A, C, D, E)]) =
11 | spawnABCD.with('A', 'B', 'C', 'D')
12 | spawnABCDE.with('a', 'c', 'd', 'e')
13 |
14 | proc detacher(query: FullQuery[(A,)], detach: Detach[(C, D, Option[E])]) =
15 | for entityId, _ in query:
16 | detach(entityId)
17 |
18 | proc assertions(
19 | findA: Query[(A,)],
20 | findB: Query[(B,)],
21 | findC: Query[(C,)],
22 | findD: Query[(D,)],
23 | findE: Query[(E,)],
24 | ) =
25 | check(findA.toSeq() == @[('a',), ('A',)])
26 | check(findB.toSeq() == @[('B',)])
27 | check(findC.toSeq().len == 0)
28 | check(findD.toSeq().len == 0)
29 | check(findE.toSeq().len == 0)
30 |
31 | proc runner(tick: proc(): void) =
32 | tick()
33 |
34 | proc myApp() {.necsus(runner, [~setup, ~detacher, ~assertions], newNecsusConf()).}
35 |
36 | test "Detaching optionals should remove them if present":
37 | myApp()
38 |
--------------------------------------------------------------------------------
/tests/t_detachRequiresAll.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | A = object
5 | B = object
6 | C = object
7 | D = object
8 | E = object
9 |
10 | proc setup(spawnABCD: Spawn[(A, B, C, D)], spawnABCDE: Spawn[(A, B, C, D, E)]) =
11 | spawnABCD.with(A(), B(), C(), D())
12 | spawnABCDE.with(A(), B(), C(), D(), E())
13 |
14 | proc detacher(query: FullQuery[(A,)], detach: Detach[(C, D, E)]) =
15 | for entityId, _ in query:
16 | detach(entityId)
17 |
18 | proc assertions(
19 | findA: Query[(A,)],
20 | findB: Query[(B,)],
21 | findC: Query[(C,)],
22 | findD: Query[(D,)],
23 | findE: Query[(E,)],
24 | ) =
25 | check(findA.items.toSeq().len == 2)
26 | check(findB.items.toSeq().len == 2)
27 | check(findC.items.toSeq().len == 1)
28 | check(findD.items.toSeq().len == 1)
29 | check(findE.items.toSeq().len == 0)
30 |
31 | proc runner(tick: proc(): void) =
32 | tick()
33 |
34 | proc myApp() {.necsus(runner, [~setup, ~detacher, ~assertions], newNecsusConf()).}
35 |
36 | test "Detaching should require all components to be present":
37 | myApp()
38 |
--------------------------------------------------------------------------------
/tests/t_detachWithoutCopy.nim:
--------------------------------------------------------------------------------
1 | import necsus/util/tools
2 |
3 | when isSinkMemoryCorruptionFixed():
4 | import unittest, necsus
5 |
6 | type
7 | A = object
8 | B = object
9 |
10 | proc `=copy`(x: var A, y: A) {.error.}
11 | proc `=copy`(x: var B, y: B) {.error.}
12 |
13 | proc exec(spawn: FullSpawn[(A, B)], detach: Detach[(B,)]) =
14 | detach(spawn.with(A(), B()))
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 |
19 | proc testDetach() {.necsus(runner, [~exec], newNecsusConf()).}
20 |
21 | test "Detaching components without requiring a copy":
22 | testDetach()
23 |
--------------------------------------------------------------------------------
/tests/t_directiveAlias.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | Thingy = object
5 | number: int
6 |
7 | Alias = Spawn[(Thingy,)]
8 |
9 | Alias2 = Alias
10 |
11 | Alias3 = Alias2
12 |
13 | SpawnTuple = (Thingy,)
14 |
15 | proc withAlias(
16 | spawn: Alias, spawn2: Alias2, spawn3: Alias3, spawn4: Spawn[SpawnTuple]
17 | ) =
18 | discard
19 |
20 | proc runner(tick: proc(): void) =
21 | tick()
22 |
23 | proc myApp() {.necsus(runner, [~withAlias], newNecsusConf()).}
24 |
25 | test "Directive aliases":
26 | myApp()
27 |
--------------------------------------------------------------------------------
/tests/t_entityDebug.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | Thingy = object
5 | number: int
6 |
7 | Whatsit = string
8 |
9 | proc spawner(spawn: Spawn[(Thingy, Whatsit)]) =
10 | spawn.with(Thingy(number: 123), "blah")
11 |
12 | proc dump(query: FullQuery[(Thingy,)], dump: EntityDebug) =
13 | for eid, _ in query:
14 | check(
15 | dump(eid) ==
16 | "EntityId(0:0) = Thingy_Whatsit (archetypeId0000); Thingy = (number: 123); Whatsit = blah"
17 | )
18 |
19 | proc runner(tick: proc(): void) =
20 | tick()
21 |
22 | proc myApp() {.necsus(runner, [~spawner, ~dump], newNecsusConf()).}
23 |
24 | test "Debugging entities":
25 | myApp()
26 |
--------------------------------------------------------------------------------
/tests/t_eventDistinction.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | EventA = int
5 |
6 | EventB = int
7 |
8 | proc publish(sendA: Outbox[EventA], sendB: Outbox[EventB]) =
9 | sendA(123)
10 | sendB(456)
11 |
12 | proc receive(receiveA: Inbox[EventA], receiveB: Inbox[EventB]) =
13 | check(receiveA.toSeq == @[123])
14 | check(receiveB.toSeq == @[456])
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 |
19 | proc testEvents() {.necsus(runner, [~publish, ~receive], newNecsusConf()).}
20 |
21 | test "Events with different names should have distinct mailboxes":
22 | testEvents()
23 |
--------------------------------------------------------------------------------
/tests/t_eventRefs.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type SomeEvent = ref object
4 | value: int
5 |
6 | proc `$`*(event: SomeEvent): string =
7 | "SomeEvent(" & $event.value & ")"
8 |
9 | proc sender(count: Local[int], emit: Outbox[SomeEvent]) =
10 | count := count.get(0) + 1
11 | for i in 0 .. count.get:
12 | emit(SomeEvent(value: i))
13 |
14 | proc receiveOne(receive: Inbox[SomeEvent], accum: Shared[int]) =
15 | for event in receive:
16 | accum := accum.get + event.value
17 |
18 | proc receiveTwo(receive: SomeEvent, accum: Shared[int]) {.eventSys.} =
19 | accum := accum.get + receive.value
20 |
21 | proc runner(accum: Shared[int], tick: proc(): void) =
22 | for i in 0 ..< 500:
23 | tick()
24 | check(accum.get == 41917000)
25 |
26 | proc testEvents() {.
27 | necsus(runner, [~sender, ~receiveOne, ~receiveTwo], newNecsusConf())
28 | .}
29 |
30 | test "Bulk sending events as refs":
31 | testEvents()
32 |
--------------------------------------------------------------------------------
/tests/t_eventSys.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc publish(sender: Outbox[string]) =
4 | sender("foo ")
5 | sender("bar ")
6 | sender("baz ")
7 |
8 | proc receive(event: string, accum: Shared[string]) {.eventSys.} =
9 | accum := accum.get & event
10 |
11 | proc runner(accum: Shared[string], tick: proc(): void) =
12 | tick()
13 | tick()
14 | check(accum.get == "foo bar baz foo bar baz ")
15 |
16 | proc testEvents() {.necsus(runner, [~publish, ~receive], newNecsusConf()).}
17 |
18 | test "Triggering event systems":
19 | testEvents()
20 |
--------------------------------------------------------------------------------
/tests/t_eventSysCall.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc publish(sender: Outbox[string]) =
4 | sender("foo")
5 |
6 | proc receive(event: string, accum: Shared[string]) {.eventSys().} =
7 | accum := event
8 |
9 | proc runner(accum: Shared[string], tick: proc(): void) =
10 | tick()
11 | check(accum.get == "foo")
12 |
13 | proc testEvents() {.necsus(runner, [~publish, ~receive], newNecsusConf()).}
14 |
15 | test "Triggering event systems when defined as pragma calls":
16 | testEvents()
17 |
--------------------------------------------------------------------------------
/tests/t_eventSysCycle.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc trigger(send: Outbox[string]) {.startupSys.} =
4 | send("Start")
5 |
6 | proc systemA(event: string, send: Outbox[int], accum: Shared[string]) {.eventSys.} =
7 | send(123)
8 | accum := accum.get & event
9 |
10 | proc systemB(event: int, send: Outbox[string], accum: Shared[string]) {.eventSys.} =
11 | send("foo")
12 | accum := accum.get & $event
13 |
14 | proc runner(accum: Shared[string], tick: proc(): void) =
15 | tick()
16 | tick()
17 | check(accum.get == "Start123foo123")
18 |
19 | proc testEvents() {.necsus(runner, [~trigger, ~systemA, ~systemB], newNecsusConf()).}
20 |
21 | test "Events that trigger a circular eventSys should cut the cycle":
22 | testEvents()
23 |
--------------------------------------------------------------------------------
/tests/t_eventSysInstanced.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc publish(sender: Outbox[string]) =
4 | sender("foo")
5 | sender("bar")
6 | sender("baz")
7 |
8 | proc receive(accum: Shared[string]): EventSystemInstance[string] {.eventSys.} =
9 | accum := "setup"
10 | return proc(event: string) =
11 | accum := accum.get & " " & event
12 |
13 | proc runner(accum: Shared[string], tick: proc(): void) =
14 | tick()
15 | tick()
16 | check(accum.get == "setup foo bar baz foo bar baz")
17 |
18 | proc testEvents() {.necsus(runner, [~publish, ~receive], newNecsusConf()).}
19 |
20 | test "Triggering instanced event systems":
21 | testEvents()
22 |
--------------------------------------------------------------------------------
/tests/t_eventUnion.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | EventA = object
5 | EventB = object
6 |
7 | proc publish(sendA: Outbox[EventA], sendB: Outbox[EventB]) =
8 | sendA(EventA())
9 | sendB(EventB())
10 |
11 | proc receive(event: EventA or EventB, output: Shared[string]) {.eventSys.} =
12 | output := output.get() & $typeof(event)
13 |
14 | proc assertions(output: Shared[string]) =
15 | check(output.get() == "EventAEventB")
16 |
17 | proc runner(tick: proc(): void) =
18 | tick()
19 |
20 | proc testEvents() {.necsus(runner, [~publish, ~receive, ~assertions], newNecsusConf()).}
21 |
22 | test "Sending to a reciever that takes a union":
23 | testEvents()
24 |
--------------------------------------------------------------------------------
/tests/t_events.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type SomeEvent = object
4 | value: int
5 |
6 | var iterations = 0
7 |
8 | proc publish(sender: Outbox[SomeEvent], loneOutbox: Outbox[string]) =
9 | for i in 1 .. 3:
10 | sender(SomeEvent(value: i + iterations))
11 |
12 | proc receive(receiver: Inbox[SomeEvent], loneInbox: Inbox[int]) =
13 | check(receiver.len == 3)
14 | check(
15 | receiver.toSeq ==
16 | @[
17 | SomeEvent(value: iterations + 1),
18 | SomeEvent(value: iterations + 2),
19 | SomeEvent(value: iterations + 3),
20 | ]
21 | )
22 |
23 | proc runner(tick: proc(): void) =
24 | tick()
25 | iterations += 10
26 | tick()
27 | iterations += 10
28 | tick()
29 |
30 | proc testEvents() {.necsus(runner, [~publish, ~receive], newNecsusConf()).}
31 |
32 | test "Sending and receiving values":
33 | testEvents()
34 |
--------------------------------------------------------------------------------
/tests/t_eventsMisordered.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | var timesThrough = 0
4 |
5 | proc receive1(receive: Inbox[int]) =
6 | case timesThrough
7 | of 0:
8 | check(receive.items.toSeq == newSeq[int]())
9 | of 1:
10 | check(receive.items.toSeq == @[0, 50])
11 | of 2:
12 | check(receive.items.toSeq == @[1, 51])
13 | else:
14 | assert(false)
15 |
16 | proc publish1(send: Outbox[int]) =
17 | send(timesThrough)
18 |
19 | proc receive2(receive: Inbox[int]) =
20 | case timesThrough
21 | of 0:
22 | check(receive.items.toSeq == @[0])
23 | of 1:
24 | check(receive.items.toSeq == @[50, 1])
25 | of 2:
26 | check(receive.items.toSeq == @[51, 2])
27 | else:
28 | assert(false)
29 |
30 | proc publish2(send: Outbox[int]) =
31 | send(timesThrough + 50)
32 |
33 | proc runner(tick: proc(): void) =
34 | tick()
35 | timesThrough += 1
36 | tick()
37 | timesThrough += 1
38 | tick()
39 |
40 | proc testEvents() {.
41 | necsus(runner, [~receive1, ~publish1, ~receive2, ~publish2], newNecsusConf())
42 | .}
43 |
44 | test "Inboxes should only be cleared after a system has executed":
45 | testEvents()
46 |
--------------------------------------------------------------------------------
/tests/t_eventsOnDisabledSystems.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | GameState = enum
5 | StateA
6 | StateB
7 |
8 | SomeEvent = int
9 |
10 | proc publish(sender: Outbox[SomeEvent], i: Shared[int]) =
11 | sender(i.get)
12 |
13 | proc receiveBefore1(receiver: Inbox[SomeEvent]) {.active(StateB).} =
14 | check(receiver.toSeq == @[1])
15 |
16 | proc receiveBefore2(value: SomeEvent) {.active(StateB), eventSys.} =
17 | check(value == 1)
18 |
19 | proc receiveBefore3(value: SomeEvent, _: Outbox[string]) {.active(StateB), eventSys.} =
20 | check(value == 1)
21 |
22 | proc changeState(state: Shared[GameState]) =
23 | state := StateB
24 |
25 | proc receiveAfter1(receiver: Inbox[SomeEvent], i: Shared[int]) {.active(StateB).} =
26 | if i.get == 0:
27 | check(receiver.len == 0)
28 | else:
29 | check(receiver.toSeq == @[1])
30 |
31 | proc receiveAfter2(value: SomeEvent) {.active(StateB), eventSys.} =
32 | check(value == 1)
33 |
34 | proc receiveAfter3(value: SomeEvent, _: Outbox[string]) {.active(StateB), eventSys.} =
35 | check(value == 1)
36 |
37 | proc runner(i: Shared[int], tick: proc(): void) =
38 | i := 0
39 | tick()
40 | i := 1
41 | tick()
42 |
43 | proc testEvents() {.
44 | necsus(
45 | runner,
46 | [
47 | ~publish,
48 | ~receiveBefore1,
49 | ~receiveBefore2,
50 | ~receiveBefore3,
51 | ~changeState,
52 | ~receiveAfter1,
53 | ~receiveAfter2,
54 | ~receiveAfter3,
55 | ],
56 | newNecsusConf(),
57 | )
58 | .}
59 |
60 | test "Events should not be sent to disabled systems":
61 | testEvents()
62 |
--------------------------------------------------------------------------------
/tests/t_eventsWithoutInbox.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type SomeEvent = object
4 |
5 | proc publish(sender: Outbox[SomeEvent]) =
6 | sender(SomeEvent())
7 |
8 | proc runner(tick: proc(): void) =
9 | tick()
10 |
11 | proc testEvents() {.necsus(runner, [~publish], newNecsusConf()).}
12 |
13 | test "Sending events without any inboxes":
14 | testEvents()
15 |
--------------------------------------------------------------------------------
/tests/t_eventsWithoutOutbox.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type SomeEvent = object
4 |
5 | proc sender(receive: Inbox[SomeEvent]) =
6 | check(receive.toSeq.len == 0)
7 |
8 | proc runner(tick: proc(): void) =
9 | tick()
10 |
11 | proc testEvents() {.necsus(runner, [~sender], newNecsusConf()).}
12 |
13 | test "Receiving events without any outboxes":
14 | testEvents()
15 |
--------------------------------------------------------------------------------
/tests/t_events_varSystem.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type SomeEvent = int
4 |
5 | proc buildSystem(): auto =
6 | return proc(listen: Inbox[SomeEvent], accum: Shared[int]) =
7 | for value in listen:
8 | accum := accum.get + value
9 |
10 | proc sender(send: Outbox[SomeEvent]) =
11 | send(7)
12 | send(1)
13 |
14 | let first = buildSystem()
15 | let second = buildSystem()
16 |
17 | proc assertions(accum: Shared[int]) =
18 | check(accum.get == 16)
19 |
20 | proc runner(tick: proc(): void) =
21 | tick()
22 |
23 | proc testEvents() {.
24 | necsus(runner, [~sender, ~first, ~second, ~assertions], newNecsusConf())
25 | .}
26 |
27 | test "Unique inboxes for systems assigned to variables":
28 | testEvents()
29 |
--------------------------------------------------------------------------------
/tests/t_genericComponents.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils, math
2 |
3 | type
4 | A = object
5 | a*: string
6 |
7 | B = object
8 | b*: string
9 |
10 | Wrap[T] = object
11 | value*: T
12 |
13 | WithStatic[T] = object
14 | value: T
15 |
16 | proc setup(
17 | spawn: Spawn[(Wrap[A], Wrap[B])],
18 | shared: Shared[Wrap[A]],
19 | ordinal: Shared[WithStatic[123]],
20 | decimal: Shared[WithStatic[3.14]],
21 | str: Shared[WithStatic["asdf"]],
22 | boolean: Shared[WithStatic[true]],
23 | character: Shared[WithStatic['a']],
24 | ) =
25 | spawn.with(Wrap[A](value: A(a: "Foo")), Wrap[B](value: B(b: "Bar")))
26 | shared.set(Wrap[A](value: A(a: "Baz")))
27 |
28 | ordinal.set(WithStatic[123](value: 123))
29 | decimal.set(WithStatic[3.14](value: 3.14))
30 | str.set(WithStatic["asdf"](value: "asdf"))
31 | boolean.set(WithStatic[true](value: true))
32 | character.set(WithStatic['a'](value: 'a'))
33 |
34 | proc assertion(
35 | all: Query[(Wrap[A], Wrap[B])],
36 | shared: Shared[Wrap[A]],
37 | ordinal: Shared[WithStatic[123]],
38 | decimal: Shared[WithStatic[3.14]],
39 | str: Shared[WithStatic["asdf"]],
40 | boolean: Shared[WithStatic[true]],
41 | character: Shared[WithStatic['a']],
42 | ) =
43 | check(toSeq(all.items).mapIt(it[0].value.a) == @["Foo"])
44 | check(toSeq(all.items).mapIt(it[1].value.b) == @["Bar"])
45 | check(shared.getOrRaise.value.a == "Baz")
46 | check(ordinal.getOrRaise.value == 123)
47 | check(decimal.getOrRaise.value == 3.14)
48 | check(str.getOrRaise.value == "asdf")
49 | check(boolean.getOrRaise.value == true)
50 | check(character.getOrRaise.value == 'a')
51 |
52 | proc runner(tick: proc(): void) =
53 | tick()
54 |
55 | proc myApp() {.necsus(runner, [~setup, ~assertion], newNecsusConf()).}
56 |
57 | test "Components with generic parameters":
58 | myApp()
59 |
--------------------------------------------------------------------------------
/tests/t_instancedObj.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type SystemInst = object
4 | value: string
5 |
6 | var execStatus = "Status:"
7 |
8 | proc initSystem(): SystemInst {.instanced.} =
9 | result.value = "foo"
10 | execStatus &= " init"
11 |
12 | proc tick(obj: var SystemInst) =
13 | check(obj.value == "foo")
14 | obj.value = "bar"
15 | execStatus &= " tick"
16 |
17 | {.warning[Deprecated]: off.}
18 | proc `=destroy`(obj: var SystemInst) {.raises: [Exception].} =
19 | # When the object is first created, it destroys the place holder. So we need to handle both
20 | check(obj.value in ["", "bar"])
21 | execStatus &= " destroy"
22 |
23 | proc runner(tick: proc(): void) =
24 | tick()
25 |
26 | proc myApp() {.necsus(runner, [~initSystem], newNecsusConf()).}
27 |
28 | test "Executed instanced systems that return objects":
29 | myApp()
30 | check(execStatus == "Status: init destroy tick destroy")
31 |
--------------------------------------------------------------------------------
/tests/t_instancedProc.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc sys1(create: Spawn[(string,)], query: Query[(string,)]): auto {.instanced.} =
4 | create.with("foo")
5 | create.with("bar")
6 | return proc() =
7 | check(query.len == 2)
8 |
9 | proc sys2(create: Spawn[(int,)], query: Query[(string,)]): SystemInstance =
10 | create.with(1)
11 | create.with(2)
12 | return proc() =
13 | check(query.len == 2)
14 |
15 | proc buildSys2(): auto =
16 | return proc(create: Spawn[(float,)], query: Query[(float,)]): SystemInstance =
17 | create.with(1.0)
18 | create.with(2.0)
19 | return proc() =
20 | check(query.len == 2)
21 |
22 | proc runner(tick: proc(): void) =
23 | tick()
24 | tick()
25 | tick()
26 |
27 | let builtSys = buildSys2()
28 |
29 | proc myApp() {.necsus(runner, [~sys1, ~sys2, ~builtSys], newNecsusConf()).}
30 |
31 | test "Executed instanced systems that return procs":
32 | myApp()
33 |
--------------------------------------------------------------------------------
/tests/t_instancedSharedVar.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc initSystem(ours: Shared[string], mine: Local[string]): auto {.instanced.} =
4 | ours := "foo"
5 | mine := "bar"
6 | return proc() =
7 | check(ours.get == "qux")
8 | check(mine.get == "bar")
9 |
10 | proc assertions(ours: Shared[string]) =
11 | check(ours.get == "foo")
12 | ours := "qux"
13 |
14 | proc runner(tick: proc(): void) =
15 | tick()
16 |
17 | proc myApp() {.necsus(runner, [~assertions, ~initSystem], newNecsusConf()).}
18 |
19 | test "Allow system variables to be instanced":
20 | myApp()
21 |
--------------------------------------------------------------------------------
/tests/t_largeEntitySets.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type Dummy = object
4 |
5 | proc runner(tick: proc(): void) =
6 | tick()
7 |
8 | proc buildSystem(size: int): auto =
9 | return proc(spawn: Spawn[(Dummy,)]) =
10 | for i in 1 .. size:
11 | spawn.with(Dummy())
12 |
13 | let system100k = buildSystem(100_000)
14 | proc hudrendThousand() {.
15 | necsus(runner, [~system100k], newNecsusConf(100_000, 100_000))
16 | .}
17 |
18 | let system1M = buildSystem(1_000_000)
19 | proc million() {.necsus(runner, [~system1M], newNecsusConf(1_000_000, 1_000_000)).}
20 |
21 | test "World with 100_000 entities":
22 | hudrendThousand()
23 |
24 | test "World with 1_000_000 entities":
25 | million()
26 |
--------------------------------------------------------------------------------
/tests/t_local.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc system1(local1: Local[string], local2: Local[string]) =
4 | if local1.isEmpty:
5 | local1 := "foo"
6 | else:
7 | check(local1.get() == "foo")
8 | if local2.isEmpty:
9 | local2 := "baz"
10 | else:
11 | check(local2.get() == "baz")
12 |
13 | proc system2(local: Local[string]) =
14 | if local.isEmpty:
15 | local := "bar"
16 | else:
17 | check(local.get() == "bar")
18 |
19 | proc runner(tick: proc(): void) =
20 | tick()
21 | tick()
22 | tick()
23 |
24 | proc testLocalVar() {.necsus(runner, [~system1, ~system2], newNecsusConf()).}
25 |
26 | test "Assigning and reading local system vars":
27 | testLocalVar()
28 |
--------------------------------------------------------------------------------
/tests/t_lookup.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, options
2 |
3 | type
4 | A = object
5 | value: int
6 |
7 | B = object
8 | value: string
9 |
10 | C = object
11 |
12 | proc spawn(spawn: Spawn[(A, B)]) =
13 | spawn.with(A(value: 1), B(value: "foo"))
14 | spawn.with(A(value: 2), B(value: "bar"))
15 |
16 | proc assertions(
17 | query: FullQuery[tuple[a: A, b: B]],
18 | lookupA: Lookup[tuple[a: A]],
19 | lookupB: Lookup[tuple[b: B]],
20 | lookupAB: Lookup[tuple[a: A, b: B]],
21 | lookupABC: Lookup[(A, B, C)],
22 | ) =
23 | for eid, comp in query:
24 | check(eid.lookupA().get().a == comp.a)
25 | check(eid.lookupB().get().b == comp.b)
26 | check(eid.lookupAB().get().a == comp.a)
27 | check(eid.lookupAB().get().b == comp.b)
28 | check(eid.lookupABC().isNone)
29 |
30 | proc runner(tick: proc(): void) =
31 | tick()
32 |
33 | proc testLookup() {.necsus(runner, [~spawn, ~assertions], newNecsusConf()).}
34 |
35 | test "Looking up components by entity Id":
36 | testLookup()
37 |
--------------------------------------------------------------------------------
/tests/t_lookupNot.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, options
2 |
3 | type
4 | A = int
5 | B = string
6 | C = float
7 |
8 | proc spawn(ab: Spawn[(A, B)], abc: Spawn[(A, B, C)]) =
9 | ab.with(1, "foo")
10 | abc.with(2, "bar", 3.14)
11 |
12 | proc assertions(query: FullQuery[(A,)], lookup: Lookup[(B, Not[C])]) =
13 | for eid, comps in query:
14 | if comps[0] == 1:
15 | check(lookup(eid).get[0] == "foo")
16 | else:
17 | check(not lookup(eid).isSome)
18 |
19 | proc runner(tick: proc(): void) =
20 | tick()
21 |
22 | proc testLookup() {.necsus(runner, [~spawn, ~assertions], newNecsusConf()).}
23 |
24 | test "Lookup with a 'Not' directive":
25 | testLookup()
26 |
--------------------------------------------------------------------------------
/tests/t_lookupPtr.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, options, sequtils
2 |
3 | type
4 | A = object
5 | value: int
6 |
7 | B = object
8 | value: string
9 |
10 | proc spawn(spawn: Spawn[(A, B)]) =
11 | spawn.with(A(value: 1), B(value: "foo"))
12 | spawn.with(A(value: 2), B(value: "bar"))
13 |
14 | proc runner(tick: proc(): void) =
15 | tick()
16 |
17 | proc modify(
18 | query: FullQuery[tuple[a: A, b: B]], lookup: Lookup[tuple[a: ptr A, b: ptr B]]
19 | ) =
20 | for eid, _ in query:
21 | eid.lookup().get().a.value = eid.lookup().get().a.value * 2
22 | eid.lookup().get().b.value = eid.lookup().get().b.value & "bar"
23 |
24 | proc assertModifications(query: Query[tuple[a: A, b: B]]) =
25 | check(query.items.toSeq.mapIt(it.a.value) == @[2, 4])
26 | check(query.items.toSeq.mapIt(it.b.value) == @["foobar", "barbar"])
27 |
28 | proc testLookupWithPointers() {.
29 | necsus(runner, [~spawn, ~modify, ~assertModifications], newNecsusConf())
30 | .}
31 |
32 | test "Modifying components from a lookup":
33 | testLookupWithPointers()
34 |
--------------------------------------------------------------------------------
/tests/t_lookupWithoutArchetypes.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = object
5 | B = object
6 | C = object
7 |
8 | proc doLookup(lookup: Lookup[(A, B, C)]) =
9 | discard
10 |
11 | proc runner(tick: proc(): void) =
12 | tick()
13 |
14 | proc testLookup() {.necsus(runner, [~doLookup], newNecsusConf()).}
15 |
16 | test "Lookups without any archetypes in the system":
17 | testLookup()
18 |
--------------------------------------------------------------------------------
/tests/t_manual.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | var ranSetup = false
4 | var ranTick = false
5 | var ranTeardown = false
6 |
7 | proc setup() =
8 | ranSetup = true
9 |
10 | proc tick() =
11 | ranTick = true
12 |
13 | proc teardown() =
14 | ranTeardown = true
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 |
19 | proc myApp() {.necsus(runner, [~setup, ~tick, ~teardown], conf = newNecsusConf()).}
20 |
21 | test "System phases should be executed when an app is run manually":
22 | block:
23 | var app: myAppState
24 | app.initMyApp()
25 | app.tick()
26 |
27 | check(ranSetup)
28 | check(ranTick)
29 | check(ranTeardown)
30 |
--------------------------------------------------------------------------------
/tests/t_maxCapacity.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | let WRAP_CAPACITY: uint8 = 5
4 |
5 | type
6 | A {.maxCapacity(2).} = object
7 | B = A
8 | Wrap[T] {.maxCapacity(WRAP_CAPACITY).} = object
9 |
10 | proc spawnToLimit[C: tuple](spawn: Spawn[C], count: auto, value: C) =
11 | for _ in 0 ..< count:
12 | necsus.set(spawn, value)
13 | expect IndexDefect:
14 | necsus.set(spawn, value)
15 |
16 | proc setup(
17 | spawn1: Spawn[(A, string)],
18 | spawn2: Spawn[(B, string)],
19 | spawn3: Spawn[(string, Wrap[A])],
20 | spawn4: Spawn[(A, Wrap[string])],
21 | ) =
22 | spawn1.spawnToLimit(2, (A(), "foo"))
23 | spawn2.spawnToLimit(2, (B(), "foo"))
24 | spawn3.spawnToLimit(5, ("foo", Wrap[A]()))
25 | spawn4.spawnToLimit(5, (A(), Wrap[string]()))
26 |
27 | proc assertion(
28 | query1: Query[(A, string)],
29 | query2: Query[(B, string)],
30 | query3: Query[(string, Wrap[A])],
31 | query4: Query[(A, Wrap[string])],
32 | ) =
33 | check(query1.len == 2)
34 | check(query2.len == 2)
35 | check(query3.len == 5)
36 | check(query4.len == 5)
37 |
38 | proc runner(tick: proc(): void) =
39 | tick()
40 |
41 | proc myApp() {.necsus(runner, [~setup, ~assertion], conf = newNecsusConf()).}
42 |
43 | test "Components with a max capacity":
44 | myApp()
45 |
--------------------------------------------------------------------------------
/tests/t_missingEntities.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[options, unittest]
2 |
3 | type
4 | Thingy = object
5 | Other = object
6 | Whatsit = object
7 |
8 | proc assertions(
9 | spawn: FullSpawn[(Thingy, Other)],
10 | find: Lookup[(Thingy,)],
11 | delete: Delete,
12 | findAgain: Query[(Thingy, Not[Whatsit])],
13 | debug: EntityDebug,
14 | attach: Attach[(Whatsit,)],
15 | detach: Detach[(Thingy,)],
16 | swap: Swap[(Whatsit,), (Thingy,)],
17 | ) =
18 | var eid = spawn.with(Thingy(), Other())
19 |
20 | check(find(eid.incGen).isNone)
21 |
22 | delete(eid.incGen)
23 | check(findAgain.len == 1)
24 |
25 | check(debug(eid.incGen) == "No such entity: EntityId(1:0)")
26 |
27 | attach(eid.incGen, (Whatsit(),))
28 | check(findAgain.len == 1)
29 |
30 | detach(eid.incGen)
31 | check(findAgain.len == 1)
32 |
33 | swap(eid.incGen, (Whatsit(),))
34 | check(findAgain.len == 1)
35 |
36 | proc runner(tick: proc(): void) =
37 | tick()
38 |
39 | proc myApp() {.necsus(runner, [~assertions], newNecsusConf()).}
40 |
41 | test "Missing entityIDs should not cause failures":
42 | myApp()
43 |
--------------------------------------------------------------------------------
/tests/t_necsusPragmas.nim:
--------------------------------------------------------------------------------
1 | import necsus, unittest
2 |
3 | proc exit(exit: Shared[NecsusRun]) =
4 | exit.set(ExitLoop)
5 |
6 | proc noRunner() {.necsus([~exit], newNecsusConf()).}
7 |
8 | test "Instantiating without specifying runner":
9 | noRunner()
10 |
--------------------------------------------------------------------------------
/tests/t_noComponents.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc someSystem() =
4 | discard
5 |
6 | proc runner(tick: proc(): void) =
7 | tick()
8 |
9 | proc myApp() {.necsus(runner, [~someSystem], newNecsusConf()).}
10 |
11 | test "Creating a world without components":
12 | myApp()
13 |
--------------------------------------------------------------------------------
/tests/t_openSym.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sets
2 |
3 | type Widget[T] = object
4 |
5 | template create(T: typedesc): untyped =
6 | proc doSetup(spawn: Spawn[(Widget[T],)]) =
7 | spawn.with(Widget[T]())
8 |
9 | proc assertions(people: Query[(ptr Widget[T],)]) =
10 | check(people.len == 1)
11 |
12 | create(string)
13 |
14 | proc runner(tick: proc(): void) =
15 | tick()
16 |
17 | proc myApp() {.necsus(runner, [~doSetup, ~assertions], conf = newNecsusConf()).}
18 |
19 | test "Parsing systems with open symbols":
20 | myApp()
21 |
--------------------------------------------------------------------------------
/tests/t_optionalQuery.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils, options, sets
2 |
3 | type
4 | A = object
5 | B = object
6 | C = object
7 | c: int
8 |
9 | D = object
10 | d: int
11 |
12 | proc setup(
13 | spawnAB: FullSpawn[(A, B)], spawnABC: Spawn[(A, B, C)], attachC: Attach[(C, D)]
14 | ) {.startupSys.} =
15 | for i in 1 .. 3:
16 | discard spawnAB.with(A(), B())
17 | spawnABC.with(A(), B(), C(c: i))
18 | spawnAB.with(A(), B()).attachC((C(c: i + 10), D(d: i + 20)))
19 |
20 | proc update(query: Query[(Option[ptr D],)]) =
21 | for (d) in query:
22 | if d.isSome:
23 | d.get().d += 30
24 |
25 | proc assertions(query: Query[(A, B, Option[C], Option[D])]) =
26 | check(query.items.toSeq.len == 9)
27 | check(
28 | query.items.toSeq.filterIt(it[2].isSome).mapIt(it[2].get().c).toHashSet ==
29 | [1, 11, 2, 12, 3, 13].toHashSet
30 | )
31 | check(query.items.toSeq.filterIt(it[2].isNone).len == 3)
32 | check(
33 | query.items.toSeq.filterIt(it[3].isSome).mapIt(it[3].get().d).toHashSet ==
34 | [51, 52, 53].toHashSet
35 | )
36 | check(query.items.toSeq.filterIt(it[3].isNone).len == 6)
37 |
38 | proc runner(tick: proc(): void) =
39 | tick()
40 |
41 | proc optionalQuery() {.necsus(runner, [~setup, ~update, ~assertions], newNecsusConf()).}
42 |
43 | test "Queries with optional components":
44 | optionalQuery()
45 |
--------------------------------------------------------------------------------
/tests/t_outsideEvents.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type SomeEvent = object
4 | value: int
5 |
6 | var expect = 0
7 |
8 | proc receive(receiver: Inbox[SomeEvent]) =
9 | check(receiver.len == expect.uint)
10 |
11 | for message in receiver:
12 | check(message.value == expect)
13 |
14 | proc testEvents() {.necsus([~receive], newNecsusConf()), used.}
15 |
16 | test "Sending events in from the outside world":
17 | var instance: testEventsState
18 | instance.initTestEvents()
19 | instance.tick()
20 |
21 | expect += 1
22 | instance.sendSomeEvent(SomeEvent(value: 1))
23 | instance.tick()
24 |
25 | expect += 1
26 | instance.sendSomeEvent(SomeEvent(value: 2))
27 | instance.sendSomeEvent(SomeEvent(value: 2))
28 | instance.tick()
29 |
--------------------------------------------------------------------------------
/tests/t_outsideEventsFromEventSys.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | SomeEvent = int
5 | OtherEvent = int
6 |
7 | var expect = 0
8 |
9 | proc receive(msg: SomeEvent) {.eventSys.} =
10 | check(msg == expect)
11 | expect += 1
12 |
13 | proc receive2(msg: OtherEvent, send: Outbox[string]) {.eventSys.} =
14 | check(msg == expect)
15 | expect += 1
16 |
17 | proc testEvents() {.necsus([~receive, ~receive2], newNecsusConf()), used.}
18 |
19 | test "Sending events in from the outside world":
20 | var instance: testEventsState
21 | instance.initTestEvents()
22 |
23 | instance.sendSomeEvent(0)
24 | instance.sendSomeEvent(1)
25 |
26 | instance.sendOtherEvent(2)
27 | instance.tick()
28 |
29 | instance.sendOtherEvent(3)
30 | instance.tick()
31 |
32 | check(expect == 4)
33 |
--------------------------------------------------------------------------------
/tests/t_passSpawn.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type A = object
4 |
5 | proc new*(spawn: Spawn[(A,)]) =
6 | spawn.with(A())
7 |
8 | proc spawner(spawn: Spawn[(A,)]) =
9 | spawn.new()
10 |
11 | proc runner(tick: proc(): void) =
12 | tick()
13 |
14 | proc myApp() {.used, necsus(runner, [~spawner], newNecsusConf()).}
15 |
16 | test "Passing spawn instance to another function":
17 | myApp()
18 |
--------------------------------------------------------------------------------
/tests/t_phases.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | var ranSetup = 0
4 | var ranTick = 0
5 | var ranTeardown = 0
6 |
7 | proc setup() {.startupSys.} =
8 | ranSetup += 1
9 |
10 | proc tick() =
11 | ranTick += 1
12 |
13 | proc teardown() {.teardownSys.} =
14 | ranTeardown += 1
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 | tick()
19 | tick()
20 | tick()
21 |
22 | proc myApp() {.necsus(runner, [~setup, ~tick, ~teardown], conf = newNecsusConf()).}
23 |
24 | test "System phases should be executed":
25 | myApp()
26 | check(ranSetup == 1)
27 | check(ranTick == 4)
28 | check(ranTeardown == 1)
29 |
--------------------------------------------------------------------------------
/tests/t_queryLen.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = object
5 | B = object
6 |
7 | proc assertion(
8 | spawn: Spawn[(A,)],
9 | attach: Attach[(B,)],
10 | detach: Detach[(B,)],
11 | delete: Delete,
12 | queryA: Query[(A,)],
13 | fullQueryA: FullQuery[(A,)],
14 | queryB: Query[(B,)],
15 | fullQueryB: FullQuery[(B,)],
16 | ) =
17 | check(queryA.len == 0)
18 | check(fullQueryA.len == 0)
19 | check(queryB.len == 0)
20 | check(fullQueryB.len == 0)
21 |
22 | for i in 1 .. 5:
23 | spawn.with(A())
24 |
25 | check(queryA.len == 5)
26 | check(queryB.len == 0)
27 |
28 | for eid, _ in fullQueryA:
29 | eid.attach((B(),))
30 |
31 | check(queryA.len == 5)
32 | check(fullQueryA.len == 5)
33 | check(queryB.len == 5)
34 | check(fullQueryB.len == 5)
35 |
36 | for eid, _ in fullQueryB:
37 | eid.detach()
38 |
39 | check(queryA.len == 5)
40 | check(fullQueryA.len == 5)
41 | check(queryB.len == 0)
42 | check(fullQueryB.len == 0)
43 |
44 | for eid, _ in fullQueryA:
45 | eid.delete()
46 |
47 | check(queryA.len == 0)
48 | check(fullQueryA.len == 0)
49 | check(queryB.len == 0)
50 | check(fullQueryB.len == 0)
51 |
52 | proc runner(tick: proc(): void) =
53 | tick()
54 |
55 | proc queryLen() {.necsus(runner, [~assertion], newNecsusConf()).}
56 |
57 | test "Report the length of a query":
58 | queryLen()
59 |
--------------------------------------------------------------------------------
/tests/t_queryNot.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | A = object
5 | phase: int
6 |
7 | B = object
8 | C = object
9 |
10 | proc setup(
11 | spawnAB: FullSpawn[(A, B)], spawnABC: Spawn[(A, B, C)], attachC: Attach[(C,)]
12 | ) =
13 | for i in 1 .. 5:
14 | discard spawnAB.with(A(phase: 1), B())
15 | spawnABC.with(A(phase: 2), B(), C())
16 | spawnAB.with(A(phase: 3), B()).attachC((C(),))
17 |
18 | proc assertions(query: Query[(A, B, Not[C])]) =
19 | check(query.items.toSeq.mapIt(it[0].phase) == @[1, 1, 1, 1, 1])
20 |
21 | proc runner(tick: proc(): void) =
22 | tick()
23 |
24 | proc notQuery() {.necsus(runner, [~setup, ~assertions], newNecsusConf()).}
25 |
26 | test "Exclude entities with a component":
27 | notQuery()
28 |
--------------------------------------------------------------------------------
/tests/t_queryOne.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, options
2 |
3 | type A = object
4 | value: string
5 |
6 | proc assertNone(query: Query[(A,)]) =
7 | check(query.single.isNone)
8 |
9 | proc setup(spawn: Spawn[(A,)]) =
10 | spawn.with(A(value: "foo"))
11 |
12 | proc assertOne(query: Query[(A,)]) =
13 | check(query.single.get()[0].value == "foo")
14 |
15 | proc runner(tick: proc(): void) =
16 | tick()
17 |
18 | proc queryOne() {.necsus(runner, [~assertNone, ~setup, ~assertOne], newNecsusConf()).}
19 |
20 | test "Pull a single value from a query":
21 | queryOne()
22 |
--------------------------------------------------------------------------------
/tests/t_queryUpdates.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | A = object
5 | B = object
6 | C = object
7 | D = object
8 |
9 | proc setup(spawn: Spawn[(A, B)]) =
10 | for i in 1 .. 5:
11 | spawn.with(A(), B())
12 |
13 | proc addC(query: FullQuery[(A, B)], attach: Attach[(C,)]) =
14 | for eid, comps in query:
15 | eid.attach((C(),))
16 |
17 | proc assertABC(query: Query[(A, B, C)]) =
18 | check(toSeq(query.items).len == 5)
19 |
20 | proc addD(query: FullQuery[(A, B)], attach: Attach[(D,)]) =
21 | for eid, comps in query:
22 | eid.attach((D(),))
23 |
24 | proc assertABCD(query: Query[(A, B, C, D)]) =
25 | check(toSeq(query.items).len == 5)
26 |
27 | proc runner(tick: proc(): void) =
28 | tick()
29 |
30 | proc attachQuery() {.
31 | necsus(runner, [~setup, ~addC, ~assertABC, ~addD, ~assertABCD], newNecsusConf())
32 | .}
33 |
34 | test "Update query when new components are attached":
35 | attachQuery()
36 |
--------------------------------------------------------------------------------
/tests/t_queryWithPointers.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | Multiply = object
5 | value*: int
6 |
7 | Add = object
8 | value*: int
9 |
10 | proc setup(spawn: Spawn[(Add, Multiply)]) =
11 | for i in 1 .. 5:
12 | spawn.with(Add(value: i), Multiply(value: i))
13 |
14 | proc operate(query: Query[tuple[mult: ptr Multiply, add: ptr Add]]) =
15 | for entity in query:
16 | entity.mult.value = entity.mult.value * entity.mult.value
17 | entity.add.value = entity.add.value + entity.add.value
18 |
19 | proc assertion(query: Query[tuple[mult: Multiply, add: Add]]) =
20 | check(toSeq(query.items).mapIt(it.mult.value) == @[1, 4, 9, 16, 25])
21 | check(toSeq(query.items).mapIt(it.add.value) == @[2, 4, 6, 8, 10])
22 |
23 | proc runner(tick: proc(): void) =
24 | tick()
25 |
26 | proc pointerQuery() {.necsus(runner, [~setup, ~operate, ~assertion], newNecsusConf()).}
27 |
28 | test "Query and update components by pointer":
29 | pointerQuery()
30 |
--------------------------------------------------------------------------------
/tests/t_queryWithoutSpawns.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = object
5 | B = object
6 | C = object
7 | D = object
8 |
9 | proc query1(query: Query[(A, B)]) =
10 | discard
11 |
12 | proc query2(query: Query[(C, D)]) =
13 | discard
14 |
15 | proc spawner(spawns: Spawn[(C,)]) =
16 | discard
17 |
18 | proc runner(tick: proc(): void) =
19 | tick()
20 |
21 | proc noSpawnQuery() {.necsus(runner, [~spawner, ~query1, ~query2], newNecsusConf()).}
22 |
23 | test "Querying for components that have never been spawned":
24 | noSpawnQuery()
25 |
--------------------------------------------------------------------------------
/tests/t_recycle.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils, algorithm
2 |
3 | type All = object
4 |
5 | proc spawn5(spawn: Spawn[(All,)]) =
6 | for i in 1 .. 5:
7 | spawn.with(All())
8 |
9 | proc assertions(all: FullQuery[(All,)]) =
10 | check(all.pairs.toSeq.mapIt(it[0].toInt.int).sorted == @[0, 1, 2, 3, 4])
11 |
12 | proc deleteAll(all: FullQuery[tuple[thingy: All]], delete: Delete) =
13 | for entityId, _ in all:
14 | delete(entityId)
15 |
16 | proc runner(tick: proc(): void) =
17 | tick()
18 | tick()
19 |
20 | proc myApp() {.necsus(runner, [~spawn5, ~assertions, ~deleteAll], newNecsusConf()).}
21 |
22 | test "Reusing deleted entityIDs":
23 | myApp()
24 |
--------------------------------------------------------------------------------
/tests/t_restore.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | RestoreMe1 = seq[string]
5 | RestoreMe2 = int
6 | RestoreMe3 = ref object
7 | number: int
8 |
9 | proc restore1(values: RestoreMe1, spawn: Spawn[(string,)]) {.restoreSys.} =
10 | for value in values:
11 | spawn.with(value)
12 |
13 | proc restore2(value: RestoreMe2, shared: Shared[int]) {.restoreSys.} =
14 | shared := value
15 |
16 | proc restore3(value: RestoreMe3, shared: Shared[RestoreMe3]) {.restoreSys.} =
17 | shared := value
18 |
19 | proc doRestore(
20 | restore: Restore,
21 | strings: Query[(string,)],
22 | restore2: Shared[int],
23 | restore3: Shared[RestoreMe3],
24 | ) =
25 | restore(
26 | """{"RestoreMe1": ["bar", "baz", "foo"], "RestoreMe2": 5, "RestoreMe3": {"number": 7}}"""
27 | )
28 | check(strings.toSeq.mapIt(it[0]) == ["bar", "baz", "foo"])
29 | check(restore2.getOrRaise == 5)
30 | check(restore3.getOrRaise.number == 7)
31 |
32 | proc runner(tick: proc(): void) =
33 | tick()
34 |
35 | proc myApp() {.
36 | necsus(runner, [~restore1, ~restore2, ~restore3, ~doRestore], newNecsusConf())
37 | .}
38 |
39 | test "Restoring system state from a string":
40 | myApp()
41 |
--------------------------------------------------------------------------------
/tests/t_restoreMissingKey.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, std/options
2 |
3 | type
4 | RestoreMe1 = string
5 | RestoreMe2 = int
6 |
7 | proc restore1(value: RestoreMe1, store: Shared[RestoreMe1]) {.restoreSys.} =
8 | store := value
9 |
10 | proc restore2(value: RestoreMe2, store: Shared[RestoreMe2]) {.restoreSys.} =
11 | store := value
12 |
13 | proc doRestore(
14 | restore: Restore, store1: Shared[RestoreMe1], store2: Shared[RestoreMe2]
15 | ) =
16 | restore("""{"RestoreMe1": "present"}""")
17 | check(store1 == "present")
18 | check(store2 == 0)
19 |
20 | proc runner(tick: proc(): void) =
21 | tick()
22 |
23 | proc myApp() {.necsus(runner, [~restore1, ~restore2, ~doRestore], newNecsusConf()).}
24 |
25 | test "Restoring system state from a string with a missing key":
26 | myApp()
27 |
--------------------------------------------------------------------------------
/tests/t_restoreWithoutSave.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type
4 | A = seq[string]
5 |
6 | B = int
7 |
8 | proc restoreA(values: A) {.restoreSys.} =
9 | discard
10 |
11 | proc saveA(): A {.saveSys.} =
12 | return @["a", "b", "c"]
13 |
14 | proc restoreB(value: B) {.restoreSys.} =
15 | discard
16 |
17 | proc doSave(save: Save, restore: Restore) =
18 | let saved = save()
19 | check(saved == """{"A":["a","b","c"]}""")
20 | restore(saved)
21 |
22 | proc runner(tick: proc(): void) =
23 | tick()
24 |
25 | proc myApp() {.
26 | necsus(runner, [~restoreA, ~saveA, ~restoreB, ~doSave], newNecsusConf())
27 | .}
28 |
29 | test "Restore system without a matching save should not produce JSON":
30 | myApp()
31 |
--------------------------------------------------------------------------------
/tests/t_runSystemOnce.nim:
--------------------------------------------------------------------------------
1 | import necsus, bundle_include, std/[options, sequtils, unittest]
2 |
3 | runSystemOnce do(
4 | str: Shared[string],
5 | integer: Local[int],
6 | spawn: FullSpawn[(string,)],
7 | find: Lookup[(string,)],
8 | query: Query[(string, int)],
9 | add: Attach[(int,)],
10 | remove: Detach[(int,)],
11 | change: Swap[(float,), (string,)],
12 | bundle: Bundle[Grouping],
13 | send: Outbox[int],
14 | receive: Inbox[int],
15 | save: Save,
16 | restore: Restore,
17 | delete: Delete,
18 | deleteAll: DeleteAll[(string,)],
19 | delta: TimeDelta,
20 | elapsed: TimeElapsed,
21 | tickId: TickId
22 | ) -> void:
23 | test "Execute a system defined via runSystemOnce":
24 | str := "foo"
25 | check(str.get == "foo")
26 |
27 | let eid = spawn.with("blah")
28 | check(find(eid) == some(("blah",)))
29 |
30 | eid.add((123,))
31 | check(query.toSeq == @[("blah", 123)])
32 | eid.remove()
33 | check(query.len == 0)
34 |
35 | send(123)
36 | check(receive.toSeq == @[123])
37 |
38 | delete(eid)
39 | check(find(eid).isNone)
40 |
41 | spawn.with("blah").change((3.1415,))
42 |
43 | restore(save())
44 |
45 | deleteAll()
46 |
47 | check(delta() <= 0.0)
48 | check(elapsed() <= 0.0)
49 |
50 | integer := 1234
51 | check(integer == 1234)
52 |
53 | check(tickId() == 0)
54 |
--------------------------------------------------------------------------------
/tests/t_runSystemOnceMultipleDefs.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | runSystemOnce do() -> void:
4 | test "Execute multiple systems in one file via runSystemOnce":
5 | discard
6 |
7 | runSystemOnce do() -> void:
8 | discard
9 |
--------------------------------------------------------------------------------
/tests/t_runnerArgs.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils, options
2 |
3 | type
4 | A = object
5 | value: int
6 |
7 | B = object
8 | C = object
9 | D = object
10 | E = object
11 | value: int
12 |
13 | proc setup(sharedVar: Shared[string], spawn: Spawn[(B, D, E)]) {.startupSys.} =
14 | sharedVar.set("foo")
15 | spawn.with(B(), D(), E(value: 789))
16 |
17 | proc runner(
18 | time: TimeDelta,
19 | sharedVar: Shared[string],
20 | spawn: Spawn[(A,)],
21 | query: FullQuery[(B,)],
22 | attach: Attach[(C,)],
23 | detachD: Detach[(D,)],
24 | lookup: Lookup[(E,)],
25 | tick: proc(): void,
26 | ) =
27 | check(sharedVar.get() == "foo")
28 | spawn.with(A(value: 123))
29 |
30 | check(query.items.toSeq.len == 1)
31 |
32 | for eid, comp in query:
33 | eid.attach((C(),))
34 | eid.detachD()
35 | check(lookup(eid).get()[0].value == 789)
36 |
37 | tick()
38 |
39 | proc assertions(checkA: Query[(A,)], checkBC: Query[(B, C)], checkD: Query[(D,)]) =
40 | check(checkA.items.toSeq.mapIt(it[0].value) == @[123])
41 | check(checkBC.items.toSeq.len == 1)
42 | check(checkD.items.toSeq.len == 0)
43 |
44 | proc testRunnerArgs() {.necsus(runner, [~setup, ~assertions], newNecsusConf()).}
45 |
46 | test "Passing directives into the runner":
47 | testRunnerArgs()
48 |
--------------------------------------------------------------------------------
/tests/t_save.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils, algorithm
2 |
3 | proc spawn(spawn: Spawn[(string,)]) =
4 | spawn.with("foo")
5 | spawn.with("bar")
6 | spawn.with("baz")
7 |
8 | type
9 | SaveMe1 = seq[string]
10 | SaveMe2 = int
11 | SaveMe3 = ref object
12 | number: int
13 |
14 | proc save1(values: Query[(string,)]): SaveMe1 {.saveSys.} =
15 | return values.mapIt(it[0]).sorted()
16 |
17 | proc save2(): SaveMe2 {.saveSys.} =
18 | return 5
19 |
20 | proc save3(): SaveMe3 {.saveSys.} =
21 | return SaveMe3(number: 7)
22 |
23 | proc doSave(save: Save) =
24 | check(
25 | save() == """{"SaveMe1":["bar","baz","foo"],"SaveMe2":5,"SaveMe3":{"number":7}}"""
26 | )
27 |
28 | proc runner(tick: proc(): void) =
29 | tick()
30 |
31 | proc myApp() {.
32 | necsus(runner, [~spawn, ~save1, ~save2, ~save3, ~doSave], newNecsusConf())
33 | .}
34 |
35 | test "Creating JSON from saveSys procs":
36 | myApp()
37 |
--------------------------------------------------------------------------------
/tests/t_saveInstanced.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type SaveMe = seq[string]
4 |
5 | proc save(): SaveSystemInstance[SaveMe] {.saveSys.} =
6 | return proc(): SaveMe =
7 | return @["a", "b", "c"]
8 |
9 | proc doSave(save: Save) =
10 | check(save() == """{"SaveMe":["a","b","c"]}""")
11 |
12 | proc runner(tick: proc(): void) =
13 | tick()
14 |
15 | proc myApp() {.necsus(runner, [~save, ~doSave], newNecsusConf()).}
16 |
17 | test "Allow saveSys sytems to be instanced":
18 | myApp()
19 |
--------------------------------------------------------------------------------
/tests/t_sharedVar.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc system1(shared1: Shared[int], shared2: Shared[string]) =
4 | if shared1.isEmpty:
5 | shared1.set(123)
6 | else:
7 | check(shared1.get() == 246)
8 |
9 | if value from shared2:
10 | check(value == "foobar")
11 | else:
12 | shared2 := "foo"
13 |
14 | proc system2(shared1: Shared[int], shared2: Shared[string]) =
15 | shared1.set(shared1.get() * 2)
16 | shared2.set(shared2.get() & "bar")
17 |
18 | proc assertions(shared1: Shared[int], shared2: Shared[string]) =
19 | check(shared1.get() in [246, 492])
20 | check(shared2.get() in ["foobar", "foobarbar"])
21 |
22 | if value from shared2:
23 | check(value in ["foobar", "foobarbar"])
24 | else:
25 | fail()
26 |
27 | proc runTwice(tick: proc(): void) =
28 | tick()
29 | tick()
30 |
31 | proc testSharedVar() {.
32 | necsus(runTwice, [~system1, ~system2, ~assertions], newNecsusConf())
33 | .}
34 |
35 | test "Assigning and reading shared system vars":
36 | testSharedVar()
37 |
38 | proc assertAppInputs(strInput: Shared[string], intInput: Shared[int]) =
39 | assert(strInput.get() == "blah blah")
40 |
41 | proc testSharedVarArg(
42 | strInput: string, intInput: int, unmentioned: float
43 | ) {.necsus(runTwice, [~assertAppInputs], newNecsusConf()).}
44 |
45 | test "Assigning shared variables from app arguments":
46 | testSharedVarArg("blah blah", 123, 3.14)
47 |
--------------------------------------------------------------------------------
/tests/t_sharedVarDefaults.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type ExampleEnum = enum
4 | A
5 | B
6 | C
7 |
8 | proc system(
9 | sharedInt: Shared[int],
10 | sharedFloat: Shared[float],
11 | sharedStr: Shared[string],
12 | sharedEnum: Shared[ExampleEnum],
13 | sharedSet: Shared[set[ExampleEnum]],
14 | sharedBool: Shared[bool],
15 | sharedSeq: Shared[seq[string]],
16 | ) =
17 | check(sharedInt.get == 0)
18 | check(sharedFloat.get == 0.0)
19 | check(sharedStr.get == "")
20 | check(sharedEnum.get == A)
21 | check(sharedSet.get == {})
22 | check(sharedBool.get == false)
23 | check(sharedSeq.get == newSeq[string]())
24 |
25 | check(sharedInt != 0)
26 | check(sharedFloat != 0.0)
27 | check(sharedStr != "")
28 | check(sharedEnum != A)
29 | check(sharedSet != {})
30 | check(sharedBool != false)
31 | check(sharedSeq != newSeq[string]())
32 |
33 | proc runOnce(tick: proc(): void) =
34 | tick()
35 |
36 | proc myApp() {.necsus(runOnce, [~system], newNecsusConf()).}
37 |
38 | test "Reading default values from shared values":
39 | myApp()
40 |
--------------------------------------------------------------------------------
/tests/t_sharedVarModify.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc system1(someVar: Shared[string]) =
4 | someVar.set("foo")
5 |
6 | proc system2(someVar: Shared[string]) =
7 | someVar.getOrRaise &= "bar"
8 |
9 | proc assertion(someVar: Shared[string]) =
10 | check(someVar.get() == "foobar")
11 |
12 | proc clearSys(someVar: Shared[string]) =
13 | check(someVar.isSome())
14 | someVar.clear()
15 |
16 | proc checkClear(someVar: Shared[string]) =
17 | check(someVar.isEmpty())
18 |
19 | proc runner(tick: proc(): void) =
20 | tick()
21 |
22 | proc testSharedVar() {.
23 | necsus(
24 | runner, [~system1, ~system2, ~assertion, ~clearSys, ~checkClear], newNecsusConf()
25 | )
26 | .}
27 |
28 | test "Modifying the value in a shared variable":
29 | testSharedVar()
30 |
--------------------------------------------------------------------------------
/tests/t_sharedVarVariousTypes.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc create(
4 | sharedTuple: Shared[(float, bool)],
5 | sharedNamedTuple: Shared[tuple[num: float, truth: bool]],
6 | sharedSeq: Shared[seq[string]],
7 | sharedArray: Shared[array[5, char]],
8 | ) =
9 | sharedTuple.set((3.14, true))
10 | sharedNamedTuple.set((2.78, false))
11 | sharedSeq.set(@["a", "b", "c"])
12 | sharedArray.set(['a', 'b', 'c', 'd', 'e'])
13 |
14 | proc assertions(
15 | sharedTuple: Shared[(float, bool)],
16 | sharedNamedTuple: Shared[tuple[num: float, truth: bool]],
17 | sharedSeq: Shared[seq[string]],
18 | sharedArray: Shared[array[5, char]],
19 | ) =
20 | check(sharedTuple.get == (3.14, true))
21 | check(sharedNamedTuple.get == (2.78, false))
22 | check(sharedSeq.get == @["a", "b", "c"])
23 | check(sharedArray.get == ['a', 'b', 'c', 'd', 'e'])
24 |
25 | proc run(tick: proc(): void) =
26 | tick()
27 |
28 | proc testSharedVar() {.necsus(run, [~create, ~assertions], newNecsusConf()).}
29 |
30 | test "Creating shared vars with various types":
31 | testSharedVar()
32 |
--------------------------------------------------------------------------------
/tests/t_sizeFromVar.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc runner(tick: proc(): void) =
4 | tick()
5 |
6 | let initialSize = 100 + 1 * 2
7 |
8 | proc myApp() {.necsus(runner, [], newNecsusConf(initialSize)).}
9 |
10 | test "Loading initial size from a variable declaration":
11 | myApp()
12 |
--------------------------------------------------------------------------------
/tests/t_spawnExtending.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, std/options
2 |
3 | type
4 | A = int
5 | B = string
6 | C = float
7 | D = bool
8 |
9 | BaseTuple = (A, C)
10 |
11 | proc spawner(spawn: Spawn[extend(BaseTuple, (B, D))]) =
12 | spawn.set(join((1, 3.14) as BaseTuple, ("bar", true) as (B, D)))
13 |
14 | proc checker(query: Query[extend(BaseTuple, (B, D))]) =
15 | check(query.single.get == (1, "bar", 3.14, true))
16 |
17 | proc runner(tick: proc(): void) =
18 | tick()
19 |
20 | proc myApp() {.necsus(runner, [~spawner, ~checker], newNecsusConf()).}
21 |
22 | test "Extending a base tuple should create a usable new tuple":
23 | myApp()
24 |
--------------------------------------------------------------------------------
/tests/t_spawnWithoutCopy.nim:
--------------------------------------------------------------------------------
1 | import necsus/util/tools
2 |
3 | when isSinkMemoryCorruptionFixed():
4 | import unittest, necsus
5 |
6 | type Thingy = object
7 | value: int
8 |
9 | proc `=copy`(target: var Thingy, source: Thingy) {.error.}
10 |
11 | proc spawner(spawn: Spawn[(Thingy,)]) =
12 | spawn.with(Thingy())
13 |
14 | proc runner(tick: proc(): void) =
15 | tick()
16 |
17 | proc myApp() {.necsus(runner, [~spawner], newNecsusConf()).}
18 |
19 | test "Spawning a value should not require a copy":
20 | myApp()
21 |
--------------------------------------------------------------------------------
/tests/t_spawn_duplicated.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, unittest]
2 |
3 | type
4 | Name = string
5 | Age = int
6 |
7 | proc setup1(spawn: Spawn[(Age, Name)]) =
8 | spawn.with(50, "Jack")
9 |
10 | proc setup2(spawn1: Spawn[(Age, Name)], spawn2: FullSpawn[(Age, Name)]) =
11 | spawn1.with(51, "Jill")
12 | discard spawn2.with(53, "Joe")
13 |
14 | proc assertion(people: Query[(Name, Age)]) =
15 | check(toSeq(people.items) == @[("Jack", 50), ("Jill", 51), ("Joe", 53)])
16 |
17 | proc runner(tick: proc(): void) =
18 | tick()
19 |
20 | proc myApp() {.necsus(runner, [~setup1, ~setup2, ~assertion], conf = newNecsusConf()).}
21 |
22 | test "Same spawn appearing multiple times":
23 | myApp()
24 |
--------------------------------------------------------------------------------
/tests/t_stateFromVar.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type GameState = enum
4 | AOnly
5 | BOnly
6 | AAndB
7 |
8 | const stateA = {AOnly, AAndB}
9 | const stateB = {BOnly, AAndB}
10 |
11 | proc always(accum: Shared[string]) =
12 | accum := accum.get("") & "|"
13 |
14 | proc whenA(accum: Shared[string]) {.active(stateA).} =
15 | accum := accum.get("") & "A"
16 |
17 | proc whenB(accum: Shared[string]) {.active(stateB).} =
18 | accum := accum.get("") & "B"
19 |
20 | proc assertion(accum: Shared[string]) {.teardownSys.} =
21 | check(accum.get == "||A|B|AB")
22 |
23 | proc runner(state: Shared[GameState], tick: proc(): void) =
24 | tick()
25 | state := AOnly
26 | tick()
27 | state := BOnly
28 | tick()
29 | state := AAndB
30 | tick()
31 |
32 | proc myApp() {.
33 | necsus(runner, [~always, ~whenA, ~whenB, ~assertion], conf = newNecsusConf())
34 | .}
35 |
36 | test "Systems should only run when their state checks are met":
37 | myApp()
38 |
--------------------------------------------------------------------------------
/tests/t_stateUnused.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type GameState = enum
4 | Example
5 |
6 | proc assertion() {.active(Example).} =
7 | discard
8 |
9 | proc runner(tick: proc(): void) =
10 | tick()
11 |
12 | proc myApp() {.necsus(runner, [~assertion], conf = newNecsusConf()).}
13 |
14 | test "A system state should compile if no systems use it as an arg":
15 | myApp()
16 |
--------------------------------------------------------------------------------
/tests/t_swap.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, sets, unittest]
2 |
3 | type
4 | A = int
5 | B = int
6 | C = int
7 | D = int
8 |
9 | proc setup(spawnNoD: Spawn[(A, B)], spawnWithD: Spawn[(A, B, D)]) =
10 | spawnNoD.with(1, 10)
11 | spawnWithD.with(2, 20, 2000)
12 |
13 | proc swapper(values: FullQuery[tuple[a: A, b: B]], swap: Swap[(C,), (B,)]) =
14 | for eid, comps in values:
15 | eid.swap((comps.a * 100,))
16 |
17 | proc assertSwapped(
18 | abc: Query[(A, B, C)], ab: Query[(A, B)], ac: Query[(A, C)], acd: Query[(A, C, D)]
19 | ) =
20 | check(toSeq(abc.items).len == 0)
21 | check(toSeq(ab.items).len == 0)
22 | check(toSeq(ac.items).toHashSet == [(1, 100), (2, 200)].toHashSet)
23 | check(toSeq(acd.items).toHashSet == [(2, 200, 2000)].toHashSet)
24 |
25 | proc runner(tick: proc(): void) =
26 | tick()
27 |
28 | proc testswap() {.necsus(runner, [~setup, ~swapper, ~assertSwapped], newNecsusConf()).}
29 |
30 | test "Swapping components":
31 | testswap()
32 |
--------------------------------------------------------------------------------
/tests/t_swapOptional.nim:
--------------------------------------------------------------------------------
1 | import necsus, std/[sequtils, options, unittest, sets]
2 |
3 | type
4 | A = int
5 | B = int
6 | C = int
7 | D = int
8 |
9 | proc setup(spawnNoD: Spawn[(A, B)], spawnWithD: Spawn[(A, B, D)]) =
10 | spawnNoD.with(1, 10)
11 | spawnWithD.with(2, 20, 2000)
12 |
13 | proc swapper(values: FullQuery[(A,)], swap: Swap[(C,), (B, Option[D])]) =
14 | for eid, (a) in values:
15 | eid.swap((a * 100,))
16 |
17 | proc assertSwapped(
18 | abc: Query[(A, B, C)], ab: Query[(A, B)], ac: Query[(A, C)], acd: Query[(A, C, D)]
19 | ) =
20 | check(toSeq(abc.items).len == 0)
21 | check(toSeq(ab.items).len == 0)
22 | check(toSeq(ac.items).toHashSet == [(2, 200), (1, 100)].toHashSet)
23 | check(toSeq(acd.items).len == 0)
24 |
25 | proc runner(tick: proc(): void) =
26 | tick()
27 |
28 | proc testswap() {.necsus(runner, [~setup, ~swapper, ~assertSwapped], newNecsusConf()).}
29 |
30 | test "Swapping components with optional detachments":
31 | testswap()
32 |
--------------------------------------------------------------------------------
/tests/t_swapRequiresAll.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | A = int
5 | B = int
6 | C = int
7 | D = int
8 |
9 | proc setup(spawnNoD: Spawn[(A, B)], spawnWithD: Spawn[(A, B, D)]) =
10 | spawnNoD.with(1, 10)
11 | spawnWithD.with(2, 20, 2000)
12 |
13 | proc swapper(values: FullQuery[tuple[a: A]], swap: Swap[(C,), (B, D)]) =
14 | for eid, comps in values:
15 | eid.swap((comps.a * 100,))
16 |
17 | proc assertSwapped(
18 | abc: Query[(A, B, C)], ab: Query[(A, B)], ac: Query[(A, C)], acd: Query[(A, C, D)]
19 | ) =
20 | check(toSeq(abc.items).len == 0)
21 | check(toSeq(acd.items).len == 0)
22 |
23 | check(toSeq(ab.items) == @[(1, 10)])
24 | check(toSeq(ac.items) == @[(2, 200)])
25 |
26 | proc runner(tick: proc(): void) =
27 | tick()
28 |
29 | proc testswap() {.necsus(runner, [~setup, ~swapper, ~assertSwapped], newNecsusConf()).}
30 |
31 | test "Swapping components":
32 | testswap()
33 |
--------------------------------------------------------------------------------
/tests/t_sysPragmas.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | var accum: string = "value:"
4 |
5 | proc atStartup() {.startupSys.} =
6 | check(accum == "value:")
7 | accum &= " startup"
8 |
9 | proc inLoop() {.loopSys.} =
10 | check(accum == "value: startup")
11 | accum &= " loop"
12 |
13 | proc atTeardown() {.teardownSys.} =
14 | check(accum == "value: startup loop")
15 | accum &= " teardown"
16 |
17 | proc runner(tick: proc(): void) =
18 | check(accum == "value: startup")
19 | tick()
20 | check(accum == "value: startup loop")
21 |
22 | proc myApp() {.necsus(runner, [~atTeardown, ~atStartup, ~inLoop], newNecsusConf()).}
23 |
24 | test "Explicitly defining the execution location for systems":
25 | myApp()
26 | check(accum == "value: startup loop teardown")
27 |
--------------------------------------------------------------------------------
/tests/t_systemState.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | type GameState = enum
4 | AOnly
5 | BOnly
6 | AAndB
7 |
8 | proc always(accum: Shared[string]) =
9 | accum := accum.get("") & "|"
10 |
11 | proc whenA(accum: Shared[string]) {.active(AOnly, AAndB).} =
12 | accum := accum.get("") & "A"
13 |
14 | proc whenB(accum: Shared[string]) {.active(BOnly, AAndB).} =
15 | accum := accum.get("") & "B"
16 |
17 | proc assertion(accum: Shared[string]) {.teardownSys.} =
18 | check(accum.get == "||A|B|AB")
19 |
20 | proc runner(state: Shared[GameState], tick: proc(): void) =
21 | tick()
22 | state := AOnly
23 | tick()
24 | state := BOnly
25 | tick()
26 | state := AAndB
27 | tick()
28 |
29 | proc myApp() {.
30 | necsus(runner, [~always, ~whenA, ~whenB, ~assertion], conf = newNecsusConf())
31 | .}
32 |
33 | test "Systems should only run when their state checks are met":
34 | myApp()
35 |
--------------------------------------------------------------------------------
/tests/t_tickId.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | var expecting = 1'u
4 |
5 | type BundledTickId = object
6 | tickId: TickId
7 |
8 | proc checkTick(tickId: TickId, tickId2: TickId, tickBundle: Bundle[BundledTickId]) =
9 | check(tickId() == expecting)
10 | check(tickId2() == expecting)
11 | check(tickBundle.tickId() == expecting)
12 | expecting += 1
13 |
14 | proc runner(tick: proc(): void) =
15 | for i in 1 .. 10:
16 | tick()
17 |
18 | proc myApp() {.necsus(runner, [~checkTick], newNecsusConf()).}
19 |
20 | test "TickId tracking":
21 | myApp()
22 |
--------------------------------------------------------------------------------
/tests/t_tickIdStorage.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus
2 |
3 | proc checkTick(tickId: TickId): auto {.instanced.} =
4 | var stored: BiggestUInt
5 | return proc() =
6 | check(tickId() != stored)
7 | stored = tickId()
8 |
9 | proc runner(tick: proc(): void) =
10 | for i in 1 .. 10:
11 | tick()
12 |
13 | proc myApp() {.necsus(runner, [~checkTick], newNecsusConf()).}
14 |
15 | test "Storing a TickId should store the value and not the pointer":
16 | myApp()
17 |
--------------------------------------------------------------------------------
/tests/t_timeDelta.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, os
2 |
3 | type Dummy = object
4 |
5 | proc setup(dt: TimeDelta, spawn: Spawn[(Dummy,)]) {.startupSys.} =
6 | check(dt() == 0)
7 |
8 | var isFirst = true
9 |
10 | proc checkTime(dt: TimeDelta) =
11 | if isFirst:
12 | isFirst = false
13 | else:
14 | check(dt() >= 0.008)
15 | sleep(10)
16 |
17 | proc runner(tick: proc(): void) =
18 | for i in 1 .. 10:
19 | tick()
20 |
21 | proc myApp() {.necsus(runner, [~setup, ~checkTime], newNecsusConf()).}
22 |
23 | test "Time delta tracking":
24 | myApp()
25 |
--------------------------------------------------------------------------------
/tests/t_timeElapsed.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, os
2 |
3 | proc setup(time: TimeElapsed) {.startupSys.} =
4 | check(time() == 0)
5 |
6 | var lastTimeCheck = 0.0
7 |
8 | proc checkTime(elapsed: TimeElapsed) =
9 | if lastTimeCheck < 0:
10 | check(elapsed() == 0)
11 | else:
12 | check(elapsed() > lastTimeCheck)
13 | check(elapsed() < lastTimeCheck + 100)
14 | lastTimeCheck = elapsed()
15 | sleep(10)
16 |
17 | proc runner(tick: proc(): void) =
18 | for i in 1 .. 10:
19 | tick()
20 |
21 | proc myApp() {.necsus(runner, [~setup, ~checkTime], newNecsusConf()).}
22 |
23 | test "Time elapsed tracking":
24 | myApp()
25 |
--------------------------------------------------------------------------------
/tests/t_tuples.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus/runtime/tuples
2 |
3 | type
4 | A = string
5 | B = int
6 | C = float
7 | D = bool
8 | E = object
9 | F = seq[int]
10 |
11 | X = object
12 | Y = object
13 | Z = object
14 |
15 | ACE = (A, C, E)
16 | BDF = (B, D, F)
17 | ABCDEF = (A, B, C, D, E, F)
18 |
19 | AB = (A, B)
20 | WithCD = extend(AB, (C, D))
21 | WithEF = extend(WithCD, (E, F))
22 |
23 | let ace: ACE = ("foo", 3.14, E())
24 | let bdf: BDF = (123, true, @[1])
25 | let abcdef: ABCDEF = ("foo", 123, 3.14, true, E(), @[1])
26 |
27 | suite "Tuple tools":
28 | test "Tuples should be extendable":
29 | check(extend(ACE, BDF) is ABCDEF)
30 | check(extend((A, C, E), BDF) is ABCDEF)
31 | check(extend(ACE, (B, D, F)) is ABCDEF)
32 | check(extend((A, C, E), (B, D, F)) is ABCDEF)
33 |
34 | check(extend(AB, (C, D), (E, F)) is ABCDEF)
35 |
36 | test "Tuples with labels should be extendable":
37 | check(extend(tuple[a: A, c: C, e: E], BDF) is ABCDEF)
38 | check(extend(ACE, tuple[b: B, d: D, f: F]) is ABCDEF)
39 | check(extend(tuple[a: A, c: C, e: E], tuple[b: B, d: D, f: F]) is ABCDEF)
40 |
41 | test "Tuples should be joinable":
42 | check(join(ace as ACE, bdf as BDF) == abcdef)
43 | check(join(ace as (A, C, E), bdf as BDF) == abcdef)
44 | check(join(ace as ACE, bdf as (B, D, F)) == abcdef)
45 | check(join(ace as (A, C, E), bdf as (B, D, F)) == abcdef)
46 |
47 | test "Tuples with labels should be joinable":
48 | check(join(ace as tuple[a: A, c: C, e: E], bdf as BDF) == abcdef)
49 | check(join(ace as ACE, bdf as tuple[b: B, d: D, f: F]) == abcdef)
50 | check(
51 | join(ace as tuple[a: A, c: C, e: E], bdf as tuple[b: B, d: D, f: F]) == abcdef
52 | )
53 |
54 | test "Tuples should be derivable from other derived tuples":
55 | check(WithCD is (A, B, C, D))
56 | check(WithEF is ABCDEF)
57 | check(join(("foo", 123, 3.14, true) as WithCD, (E(), @[1]) as (E, F)) == abcdef)
58 |
59 | test "Join multiple tuple types":
60 | let joined = join(
61 | ("foo",) as (A,), (123,) as (B,), (3.14, true) as (C, D), (E(), @[1]) as (E, F)
62 | )
63 |
64 | check(joined == abcdef)
65 |
66 | test "Join without as":
67 | let joined = join((X(), E()), (Z(), Y()), ("foo",) as (A,), (123,) as (B,))
68 |
69 | check(joined == ("foo", 123, E(), X(), Y(), Z()))
70 |
--------------------------------------------------------------------------------
/tests/t_update.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, sequtils
2 |
3 | type
4 | Name = object
5 | name*: string
6 |
7 | Age = object
8 | age*: int
9 |
10 | Mood = object
11 | mood*: string
12 |
13 | proc setup(spawn: Spawn[(Age, Mood, Name)]) =
14 | spawn.with(Age(age: 20), Mood(mood: "Happy"), Name(name: "Foo"))
15 | spawn.with(Age(age: 30), Mood(mood: "Sad"), Name(name: "Bar"))
16 |
17 | proc modify(all: FullQuery[(Age, Mood)], attach: Attach[(Age, Mood)]) =
18 | for entityId, info in all:
19 | let newAge = Age(age: info[0].age + 1)
20 | let newMood = Mood(mood: "Very " & info[1].mood)
21 | entityId.attach((newAge, newMood))
22 |
23 | proc assertions(all: Query[(Name, Age, Mood)]) =
24 | check(toSeq(all.items).mapIt(it[0].name) == @["Foo", "Bar"])
25 | check(toSeq(all.items).mapIt(it[1].age) == @[21, 31])
26 | check(toSeq(all.items).mapIt(it[2].mood) == @["Very Happy", "Very Sad"])
27 |
28 | proc runner(tick: proc(): void) =
29 | tick()
30 |
31 | proc testAttaches() {.necsus(runner, [~setup, ~modify, ~assertions], newNecsusConf()).}
32 |
33 | test "Updating components via an Attach":
34 | testAttaches()
35 |
--------------------------------------------------------------------------------
/tests/t_variableSystem.nim:
--------------------------------------------------------------------------------
1 | import unittest, necsus, options
2 |
3 | type A = object
4 |
5 | const create = proc(spawn: Spawn[(A,)]) =
6 | spawn.with(A())
7 | spawn.with(A())
8 | spawn.with(A())
9 |
10 | const check = proc(query: Query[(A,)]) =
11 | check(query.len == 3)
12 |
13 | proc runner(tick: proc(): void) =
14 | tick()
15 |
16 | proc variableApp() {.necsus(runner, [~create, ~check], newNecsusConf()).}
17 |
18 | test "Allow systems to be create from variables":
19 | variableApp()
20 |
--------------------------------------------------------------------------------