├── .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 | --------------------------------------------------------------------------------