From 9163285f9e0503c7d485ccd739cc786a8352a9b2 Mon Sep 17 00:00:00 2001 From: as8d <70523631+as8d@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:47:39 +0200 Subject: [PATCH 1/4] Add a replication and input section and add feces and axis to addons.md (#217) --- docs/learn/concepts/addons.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/learn/concepts/addons.md b/docs/learn/concepts/addons.md index 8a7ba01..a40adfb 100644 --- a/docs/learn/concepts/addons.md +++ b/docs/learn/concepts/addons.md @@ -41,4 +41,16 @@ Provides hooks and a scheduler that implements jabby and a topographical runtime An agnostic scheduler inspired by Bevy and Flecs, with core features including phases, pipelines, run conditions, and startup systems. Planck also provides plugins for Jabby, Matter Hooks, and more. +# Replication + +## [feces](https://github.com/NeonD00m/feces) + +A generalized replication system for jecs + +# Input + +## [Axis](https://github.com/NeonD00m/axis) + +An agnostic, simple and versatile input library for ECS + # Observers From e5a30f2bc74430da11cbaa523392dacd77e1d10d Mon Sep 17 00:00:00 2001 From: Neon Date: Thu, 17 Apr 2025 12:08:32 -0700 Subject: [PATCH 2/4] Fix jecs addon repository links --- docs/learn/concepts/addons.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/learn/concepts/addons.md b/docs/learn/concepts/addons.md index a40adfb..769baa9 100644 --- a/docs/learn/concepts/addons.md +++ b/docs/learn/concepts/addons.md @@ -8,17 +8,17 @@ A collection of third-party jecs addons made by the community. If you would like A jecs debugger with a string-based query language and entity editing capabilities. -## [jecs_entity_visualiser](https://github.com/Ukendio/jecs/tree/main/addons/entity_visualiser) +## [jecs_entity_visualiser](https://github.com/Ukendio/jecs/blob/main/addons/entity_visualiser.luau) A simple entity and component visualiser in the output -## [jecs_lifetime_tracker](https://github.com/Ukendio/jecs/tree/main/addons/lifetime_tracker) +## [jecs_lifetime_tracker](https://github.com/Ukendio/jecs/blob/main/addons/lifetime_tracker.luau) A tool for inspecting entity lifetimes # Helpers -## [jecs_observers](https://github.com/Ukendio/jecs/tree/main/addons/observers) +## [jecs_observers](https://github.com/Ukendio/jecs/blob/main/addons/observers.luau) Observers for queries and signals for components From 6835f91a092f2fa6d38dafe0d6f324d35903cf94 Mon Sep 17 00:00:00 2001 From: Axen Date: Tue, 22 Apr 2025 05:37:04 +0300 Subject: [PATCH 3/4] Docs/hooks set order warning (#220) * Add docs warning about hook set order * Improve warning wording --- docs/learn/concepts/component-traits.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/learn/concepts/component-traits.md b/docs/learn/concepts/component-traits.md index 3b0f1ce..10aa99d 100644 --- a/docs/learn/concepts/component-traits.md +++ b/docs/learn/concepts/component-traits.md @@ -14,6 +14,10 @@ A (component) ID can be marked with `Tag“ in which the component will never con Hooks are part of the "interface" of a component. You could consider hooks as the counterpart to OOP methods in ECS. They define the behavior of a component, but can only be invoked through mutations on the component data. You can only configure a single `OnAdd`, `OnRemove` and `OnSet` hook per component, just like you can only have a single constructor and destructor. +::: warning +Hooks, added to a component that has already been added to other entities/components, will not be called. +::: + ## Examples ::: code-group From 42401f93ae8f974a438ce73ee00a25db572626ba Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 22 Apr 2025 04:38:30 +0200 Subject: [PATCH 4/4] Add component registration and metadata API (#219) * Add component registration and metadata API * Fix test case erroring * Fix type issues * Add component to hook calls' arguments * Add is_tag function * Allow focus to capture a single Test * Add test cases for preregistered tags --- jecs.luau | 271 +++++++++++++++++++++++-------------- test/addons/observers.luau | 19 +++ test/tests.luau | 46 +++++-- tools/testkit.luau | 13 +- 4 files changed, 231 insertions(+), 118 deletions(-) diff --git a/jecs.luau b/jecs.luau index ce9e63d..3f297da 100644 --- a/jecs.luau +++ b/jecs.luau @@ -45,9 +45,9 @@ type ecs_id_record_t = { flags: number, size: number, hooks: { - on_add: ((entity: i53, data: any?) -> ())?, - on_change: ((entity: i53, data: any) -> ())?, - on_remove: ((entity: i53) -> ())?, + on_add: ((entity: i53, id: i53, data: any?) -> ())?, + on_change: ((entity: i53, id: i53, data: any) -> ())?, + on_remove: ((entity: i53, id: i53) -> ())?, }, } @@ -116,13 +116,47 @@ local ECS_ID_MASK = 0b00 local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_GENERATION_MASK = bit32.lshift(1, 16) -local NULL_ARRAY = table.freeze({}) +local NULL_ARRAY = table.freeze({}) :: Column +local NULL = newproxy(false) + local ECS_INTERNAL_ERROR = [[ This is an internal error, please file a bug report via the following link: https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md ]] +local ecs_metadata: Map> = {} +local ecs_max_component_id = 0 +local ecs_max_tag_id = EcsRest + +local function ECS_COMPONENT() + ecs_max_component_id += 1 + if ecs_max_component_id > HI_COMPONENT_ID then + error("Too many components") + end + return ecs_max_component_id +end + +local function ECS_TAG() + ecs_max_tag_id += 1 + return ecs_max_tag_id +end + +local function ECS_META(id: i53, ty: i53, value: any?) + local bundle = ecs_metadata[id] + if bundle == nil then + bundle = {} + ecs_metadata[id] = bundle + end + bundle[ty] = if value == nil then NULL else value +end + +local function ECS_META_RESET() + ecs_metadata = {} + ecs_max_component_id = 0 + ecs_max_tag_id = EcsRest +end + local function ECS_COMBINE(id: number, generation: number): i53 return id + (generation * ECS_ENTITY_MASK) end @@ -466,6 +500,14 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b return records[id] ~= nil end +local function ecs_is_tag(world: ecs_world_t, entity: i53): boolean + local idr = world.component_index[entity] + if idr then + return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 + end + return not world_has_one_inline(world, entity, EcsComponent) +end + local function world_has(world: ecs_world_t, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean @@ -529,62 +571,64 @@ end local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t local component_index = world.component_index local entity_index = world.entity_index - local idr: ecs_id_record_t = component_index[id] + local idr: ecs_id_record_t? = component_index[id] - if not idr then - local flags = ECS_ID_MASK - local relation = id - local target = 0 - local is_pair = ECS_IS_PAIR(id) - if is_pair then - relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 - assert(relation and entity_index_is_alive( - entity_index, relation), ECS_INTERNAL_ERROR) - target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 - assert(target and entity_index_is_alive( - entity_index, target), ECS_INTERNAL_ERROR) - end - - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) - - local has_delete = false - - if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then - has_delete = true - end - - local on_add, on_change, on_remove = world_get(world, - relation, EcsOnAdd, EcsOnChange, EcsOnRemove) - - local is_tag = not world_has_one_inline(world, - relation, EcsComponent) - - if is_tag and is_pair then - is_tag = not world_has_one_inline(world, target, EcsComponent) - end - - flags = bit32.bor( - flags, - if has_delete then ECS_ID_DELETE else 0, - if is_tag then ECS_ID_IS_TAG else 0 - ) - - idr = { - size = 0, - cache = {}, - counts = {}, - flags = flags, - hooks = { - on_add = on_add, - on_change = on_change, - on_remove = on_remove, - }, - } - - component_index[id] = idr + if idr then + return idr end + local flags = ECS_ID_MASK + local relation = id + local target = 0 + local is_pair = ECS_IS_PAIR(id) + if is_pair then + relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 + assert(relation and entity_index_is_alive( + entity_index, relation), ECS_INTERNAL_ERROR) + target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 + assert(target and entity_index_is_alive( + entity_index, target), ECS_INTERNAL_ERROR) + end + + local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) + local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) + + local has_delete = false + + if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then + has_delete = true + end + + local on_add, on_change, on_remove = world_get(world, + relation, EcsOnAdd, EcsOnChange, EcsOnRemove) + + local is_tag = not world_has_one_inline(world, + relation, EcsComponent) + + if is_tag and is_pair then + is_tag = not world_has_one_inline(world, target, EcsComponent) + end + + flags = bit32.bor( + flags, + if has_delete then ECS_ID_DELETE else 0, + if is_tag then ECS_ID_IS_TAG else 0 + ) + + idr = { + size = 0, + cache = {}, + counts = {}, + flags = flags, + hooks = { + on_add = on_add, + on_change = on_change, + on_remove = on_remove, + }, + } :: ecs_id_record_t + + component_index[id] = idr + return idr end @@ -621,7 +665,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: local columns = (table.create(length) :: any) :: { Column } local records: { number } = {} - local counts: {number} = {} + local counts: { number } = {} local archetype: ecs_archetype_t = { columns = columns, @@ -670,7 +714,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} + world.archetype_edges[archetype.id] = {} :: Map return archetype end @@ -747,17 +791,17 @@ local function archetype_traverse_remove( local edges = world.archetype_edges local edge = edges[from.id] - local to = edge[id] - if not to then + local to: ecs_archetype_t = edge[id] + if to == nil then to = find_archetype_without(world, from, id) edge[id] = to edges[to.id][id] = from end - return to :: ecs_archetype_t + return to end -local function find_archetype_with(world, id, from) +local function find_archetype_with(world, id, from): ecs_archetype_t local id_types = from.types local at = find_insert(id_types, id) @@ -767,7 +811,7 @@ local function find_archetype_with(world, id, from) return archetype_ensure(world, dst) end -local function archetype_traverse_add(world, id, from: ecs_archetype_t) +local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_archetype_t from = from or world.ROOT_ARCHETYPE if from.records[id] then return from @@ -813,7 +857,7 @@ local function world_add( local on_add = idr.hooks.on_add if on_add then - on_add(entity) + on_add(entity, id) end end @@ -830,7 +874,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown local idr_hooks = idr.hooks if from == to then - local tr = to.records[id] + local tr = (to :: ecs_archetype_t).records[id] local column = from.columns[tr] column[record.row] = data @@ -838,7 +882,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown -- and just set the data directly. local on_change = idr_hooks.on_change if on_change then - on_change(entity, data) + on_change(entity, id, data) end return @@ -861,7 +905,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown local on_add = idr_hooks.on_add if on_add then - on_add(entity, data) + on_add(entity, id, data) end end @@ -893,7 +937,7 @@ local function world_remove(world: ecs_world_t, entity: i53, id: i53) local idr = world.component_index[id] local on_remove = idr.hooks.on_remove if on_remove then - on_remove(entity) + on_remove(entity, id) end local to = archetype_traverse_remove(world, id, record.archetype) @@ -945,7 +989,7 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, local idr = component_index[id] local on_remove = idr.hooks.on_remove if on_remove then - on_remove(delete) + on_remove(delete, id) end end @@ -984,8 +1028,8 @@ local function world_clear(world: ecs_world_t, entity: i53) end if idr_t then - local queue - local ids + local queue: { i53 } + local ids: Map local count = 0 local archetype_ids = idr_t.cache @@ -1005,7 +1049,7 @@ local function world_clear(world: ecs_world_t, entity: i53) continue end if not ids then - ids = {} + ids = {} :: { [i53]: boolean } end ids[id] = true removal_queued = true @@ -1016,7 +1060,7 @@ local function world_clear(world: ecs_world_t, entity: i53) end if not queue then - queue = {} + queue = {} :: { i53 } end local n = #entities @@ -1175,8 +1219,8 @@ local function world_delete(world: ecs_world_t, entity: i53) end if idr_t then - local children - local ids + local children: { i53 } + local ids: Map local count = 0 local archetype_ids = idr_t.cache @@ -1206,7 +1250,7 @@ local function world_delete(world: ecs_world_t, entity: i53) break else if not ids then - ids = {} + ids = {} :: { [i53]: boolean } end ids[id] = true removal_queued = true @@ -1217,7 +1261,7 @@ local function world_delete(world: ecs_world_t, entity: i53) continue end if not children then - children = {} + children = {} :: { i53 } end local n = #entities table.move(entities, 1, n, count + 1, children) @@ -1240,7 +1284,7 @@ local function world_delete(world: ecs_world_t, entity: i53) if idr_r then local archetype_ids = idr_r.cache local flags = idr_r.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then + if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then for archetype_id in archetype_ids do local idr_r_archetype = archetypes[archetype_id] local entities = idr_r_archetype.entities @@ -1754,7 +1798,7 @@ local function query_cached(query: ecs_query_data_t) local observable = world.observable :: ecs_observable_t local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then - on_create_action = {} + on_create_action = {} :: Map observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] @@ -1765,7 +1809,7 @@ local function query_cached(query: ecs_query_data_t) local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then - on_delete_action = {} + on_delete_action = {} :: Map observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] @@ -2168,12 +2212,12 @@ local function world_query(world: ecs_world_t, ...) return q end - if idr == nil or map.size < idr.size then + if idr == nil or (map.size :: number) < (idr.size :: number) then idr = map end end - if not idr then + if idr == nil then return q end @@ -2306,7 +2350,7 @@ local function world_new() ROOT_ARCHETYPE = (nil :: any) :: Archetype, max_archetype_id = 0, - max_component_id = 0, + max_component_id = ecs_max_component_id, observable = {} :: Observable, }, World) :: any @@ -2323,6 +2367,21 @@ local function world_new() entity_index_new_id(entity_index) end + for i = EcsRest + 1, ecs_max_tag_id do + -- Initialize built-in components + entity_index_new_id(entity_index) + end + + for i, bundle in ecs_metadata do + for ty, value in bundle do + if value == NULL then + world_add(self, i, ty) + else + world_set(self, i, ty, value) + end + end + end + world_add(self, EcsName, EcsComponent) world_add(self, EcsOnChange, EcsComponent) world_add(self, EcsOnAdd, EcsComponent) @@ -2350,22 +2409,25 @@ end World.new = world_new -export type Entity = { __T: T } -export type Id = { __T: T } +export type Entity = { __T: T } +export type Id = { __T: T } export type Pair = Id

type ecs_id_t = Id | Pair | Pair<"Tag", T> export type Item = (self: Query) -> (Entity, T...) export type Iter = (query: Query) -> () -> (Entity, T...) -export type Query = typeof(setmetatable({}, { - __iter = (nil :: any) :: Iter, -})) & { - iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, -} +export type Query = typeof(setmetatable( + {} :: { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, + }, + {} :: { + __iter: Iter + } +)) export type Observer = { callback: (archetype: Archetype) -> (), @@ -2399,20 +2461,20 @@ export type World = { component: (self: World) -> Entity, --- Gets the target of an relationship. For example, when a user calls --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. - target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, + target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, --- Deletes an entity and all it's related components and relationships. delete: (self: World, id: Entity) -> (), --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), + add: (self: World, id: Entity, component: Id) -> (), --- Assigns a value to a component on the given entity set: (self: World, id: Entity, component: Id, data: T) -> (), cleanup: (self: World) -> (), -- Clears an entity from the world - clear: (self: World, id: Entity) -> (), + clear: (self: World, id: Id) -> (), --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), + remove: (self: World, id: Entity, component: Id) -> (), --- Retrieves the value of up to 4 components. These values may be nil. get: ((self: World, id: Entity, Id) -> A?) & ((self: World, id: Entity, Id, Id) -> (A?, B?)) @@ -2431,9 +2493,9 @@ export type World = { --- Checks if the world contains the given entity contains:(self: World, entity: Entity) -> boolean, - each: (self: World, id: Id) -> () -> Entity, + each: (self: World, id: Id) -> () -> Entity, - children: (self: World, id: Id) -> () -> Entity, + children: (self: World, id: Id) -> () -> Entity, --- Searches the world for entities that match a given query query: ((World, Id) -> Query) @@ -2461,10 +2523,15 @@ export type World = { -- return first -- end -- end +-- return { World = World :: { new: () -> World }, world = world_new :: () -> World, + component = (ECS_COMPONENT :: any) :: () -> Entity, + tag = (ECS_TAG :: any) :: () -> Entity, + meta = (ECS_META :: any) :: (id: Entity, id: Id, value: T) -> Entity, + is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, @@ -2487,8 +2554,8 @@ return { ECS_GENERATION_INC = ECS_GENERATION_INC, ECS_GENERATION = ECS_GENERATION, ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, - ECS_ID_DELETE = ECS_ID_DELETE, + ECS_META_RESET = ECS_META_RESET, IS_PAIR = (ECS_IS_PAIR :: any) :: (pair: Pair) -> boolean, pair_first = (ecs_pair_first :: any) :: (world: World, pair: Pair) -> Id

, diff --git a/test/addons/observers.luau b/test/addons/observers.luau index cf5e10a..11cac9f 100644 --- a/test/addons/observers.luau +++ b/test/addons/observers.luau @@ -82,6 +82,25 @@ TEST("addons/observers", function() world:set(e, A, true) CHECK(count == 3) end + + do CASE "Call on pairs" + local A = world:component() + + local callcount = 0 + world:added(A, function(entity) + callcount += 1 + end) + world:added(A, function(entity) + callcount += 1 + end) + + local e = world:entity() + local e1 = world:entity() + + world:add(e1, jecs.pair(A, e)) + world:add(e, jecs.pair(A, e1)) + CHECK(callcount == 4) + end end) return FINISH() diff --git a/test/tests.luau b/test/tests.luau index cb95c90..16f21d6 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -22,6 +22,7 @@ type Entity = jecs.Entity type Id = jecs.Id local entity_visualiser = require("@tools/entity_visualiser") +local lifetime_tracker_add = require("@tools/lifetime_tracker") local dwi = entity_visualiser.stringify TEST("world:add()", function() @@ -62,8 +63,8 @@ end) TEST("world:children()", function() local world = jecs.world() - local C = world:component() - local T = world:entity() + local C = jecs.component() + local T = jecs.tag() local e1 = world:entity() world:set(e1, C, true) @@ -95,6 +96,8 @@ TEST("world:children()", function() end CHECK(count == 1) + + jecs.ECS_META_RESET() end) TEST("world:clear()", function() @@ -195,6 +198,26 @@ TEST("world:clear()", function() end) TEST("world:component()", function() + do CASE "allow IDs to be registered" + local A = jecs.component() + local B = jecs.component() + + local world = jecs.world() + local C = world:component() + CHECK((A :: any) == 1) + CHECK((B :: any) == 2) + CHECK((C :: any) == 3) + + local e = world:entity() + + world:set(e, A, "foo") + world:set(e, B, "foo") + world:set(e, C, "foo") + + CHECK(world:has(e, A, B, C)) + + jecs.ECS_META_RESET() -- Reset the ECS metadata because they may have side effects + end do CASE "only components should have EcsComponent trait" local world = jecs.world() local A = world:component() @@ -224,6 +247,7 @@ TEST("world:component()", function() end) TEST("world:contains()", function() + local tag = jecs.tag() local world = jecs.world() local id = world:entity() CHECK(world:contains(id)) @@ -233,6 +257,9 @@ TEST("world:contains()", function() world:delete(id) CHECK(not world:contains(id)) end + + CHECK(world:contains(tag)) + jecs.ECS_META_RESET() end) TEST("world:delete()", function() @@ -606,6 +633,7 @@ end) TEST("world:entity()", function() local N = 2^8 + do CASE "unique IDs" local world = jecs.world() local set = {} @@ -720,21 +748,21 @@ TEST("world:query()", function() local i = 0 local iter = 0 - for _, e in q:iter() do + for _ in q:iter() do iter += 1 i=1 end CHECK (iter == 1) CHECK(i == 1) - for _, e in q:iter() do + for _ in q:iter() do i=2 end CHECK(i == 2) - for _, e in q :: any do + for _ in q do i=3 end CHECK(i == 3) - for _, e in q :: any do + for _ in q do i=4 end CHECK(i == 4) @@ -746,8 +774,8 @@ TEST("world:query()", function() end do CASE "multiple iter" local world = jecs.World.new() - local A = world:component() - local B = world:component() + local A = world:component() :: jecs.Entity + local B = world:component() :: jecs.Entity local e = world:entity() world:add(e, A) world:add(e, B) @@ -1405,8 +1433,6 @@ TEST("#adding a recycled target", function() end) - - TEST("#repro2", function() local world = jecs.world() local Lifetime = world:component() :: Id diff --git a/tools/testkit.luau b/tools/testkit.luau index 223eb0c..cb92c49 100644 --- a/tools/testkit.luau +++ b/tools/testkit.luau @@ -226,6 +226,8 @@ local function CHECK(value: T, stack: number?): T? return value end +local test_focused = false + local function TEST(name: string, fn: () -> ()) test = { @@ -238,6 +240,10 @@ local function TEST(name: string, fn: () -> ()) local t = test + if check_for_focused and not test_focused then + test.focus = true + test_focused = true + end table.insert(tests, t) end @@ -245,15 +251,10 @@ local function FOCUS() assert(test, "no active test") check_for_focused = true - if test.case then - test.case.focus = true - else - test.focus = true - end + test_focused = false end local function FINISH(): number - local success = true local total_cases = 0 local passed_cases = 0 local passed_focus_cases = 0