Fix line endings

This commit is contained in:
Ukendio 2025-06-01 15:47:58 +02:00
parent bb03e88d3d
commit b73bb0daee
21 changed files with 2102 additions and 2011 deletions

View file

@ -1,49 +1,49 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local C1 = ecs:component()
local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component()
local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
return {
ParameterGenerator = function()
end,
Functions = {
Mirror = function()
local m = mcs:entity()
for i = 1, 100 do
mcs:add(m, E3)
mcs:remove(m, E3)
end
end,
Jecs = function()
local j = ecs:entity()
for i = 1, 100 do
ecs:add(j, C3)
ecs:remove(j, C3)
end
end,
},
}
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local C1 = ecs:component()
local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component()
local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
return {
ParameterGenerator = function()
end,
Functions = {
Mirror = function()
local m = mcs:entity()
for i = 1, 100 do
mcs:add(m, E3)
mcs:remove(m, E3)
end
end,
Jecs = function()
local j = ecs:entity()
for i = 1, 100 do
ecs:add(j, C3)
ecs:remove(j, C3)
end
end,
},
}

View file

@ -1,28 +1,28 @@
local function collect<T...>(
signal: {
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
): () -> (T...)
local enqueued = {}
local i = 0
local connection = (signal :: any):Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
return collect
local function collect<T...>(
signal: {
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
): () -> (T...)
local enqueued = {}
local i = 0
local connection = (signal :: any):Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
return collect

View file

@ -1,36 +1,36 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local types = require("./types")
local Networked = jecs.tag()
local NetworkedPair = jecs.tag()
local Renderable = jecs.component() :: jecs.Id<Instance>
jecs.meta(Renderable, Networked)
local Poison = jecs.component() :: jecs.Id<number>
jecs.meta(Poison, Networked)
local Health = jecs.component() :: jecs.Id<number>
jecs.meta(Health, Networked)
local Player = jecs.component() :: jecs.Id<Player>
jecs.meta(Player, Networked)
local components = {
Renderable = Renderable,
Player = Player,
Poison = Poison,
Health = Health,
Networked = Networked,
NetworkedPair = NetworkedPair,
}
for name, component in components do
jecs.meta(component, jecs.Name, name)
end
return components
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local types = require("./types")
local Networked = jecs.tag()
local NetworkedPair = jecs.tag()
local Renderable = jecs.component() :: jecs.Id<Instance>
jecs.meta(Renderable, Networked)
local Poison = jecs.component() :: jecs.Id<number>
jecs.meta(Poison, Networked)
local Health = jecs.component() :: jecs.Id<number>
jecs.meta(Health, Networked)
local Player = jecs.component() :: jecs.Id<Player>
jecs.meta(Player, Networked)
local components = {
Renderable = Renderable,
Player = Player,
Poison = Poison,
Health = Health,
Networked = Networked,
NetworkedPair = NetworkedPair,
}
for name, component in components do
jecs.meta(component, jecs.Name, name)
end
return components

View file

@ -1,190 +1,190 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
type Observer<T...> = {
callback: (jecs.Entity) -> (),
query: jecs.Query<T...>,
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (),
monitor: (PatchedWorld, Observer<any>) -> (),
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
local signals = {
added = {},
emplaced = {},
removed = {},
deleted = {}
}
world = world :: jecs.World & {[string]: any}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
idr.hooks.on_add = on_add :: any
idr_r.hooks.on_add = on_add :: any
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_change(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnChange, on_change)
idr.hooks.on_change = on_change :: any
idr_r.hooks.on_change = on_change :: any
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_remove(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnRemove, on_remove)
idr.hooks.on_remove = on_remove :: any
idr_r.hooks.on_remove = on_remove :: any
end
table.insert(listeners, fn)
end
world.signals = signals
world.observer = observers_new
world.monitor = monitors_new
-- local world_delete = world.delete
-- world.deleted = function(_, fn)
-- local listeners = signals.deleted
-- table.insert(listeners, fn)
-- end
-- world.delete = function(world, entity)
-- world_delete(world, entity)
-- for _, fn in signals.deleted do
-- fn(entity)
-- end
-- end
return world :: PatchedWorld
end
return observers_add
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
type Observer<T...> = {
callback: (jecs.Entity) -> (),
query: jecs.Query<T...>,
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (),
monitor: (PatchedWorld, Observer<any>) -> (),
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
local signals = {
added = {},
emplaced = {},
removed = {},
deleted = {}
}
world = world :: jecs.World & {[string]: any}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
idr.hooks.on_add = on_add :: any
idr_r.hooks.on_add = on_add :: any
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_change(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnChange, on_change)
idr.hooks.on_change = on_change :: any
idr_r.hooks.on_change = on_change :: any
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_remove(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnRemove, on_remove)
idr.hooks.on_remove = on_remove :: any
idr_r.hooks.on_remove = on_remove :: any
end
table.insert(listeners, fn)
end
world.signals = signals
world.observer = observers_new
world.monitor = monitors_new
-- local world_delete = world.delete
-- world.deleted = function(_, fn)
-- local listeners = signals.deleted
-- table.insert(listeners, fn)
-- end
-- world.delete = function(world, entity)
-- world_delete(world, entity)
-- for _, fn in signals.deleted do
-- fn(entity)
-- end
-- end
return world :: PatchedWorld
end
return observers_add

View file

@ -1,50 +1,50 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = {
FireClient: (Remote<T...>, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>) -> (),
OnServerEvent: {
Connect: (any, fn: (Player, T...) -> () ) -> ()
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
}
local function stream_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
local function datagram_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<{
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}>,
}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = {
FireClient: (Remote<T...>, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>) -> (),
OnServerEvent: {
Connect: (any, fn: (Player, T...) -> () ) -> ()
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
}
local function stream_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
local function datagram_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<{
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}>,
}

View file

@ -1,136 +1,136 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
jabby.set_check_function(function() return true end)
local scheduler = jabby.scheduler.create("jabby scheduler")
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = scheduler,
},
})
local ContextActionService = game:GetService("ContextActionService")
local function create_widget(_, state: Enum.UserInputState)
local client = jabby.obtain_client()
if state ~= Enum.UserInputState.Begin then return end
client.spawn_app(client.apps.home, nil)
end
local RunService = game:GetService("RunService")
local System = jecs.component() :: jecs.Id<{
fn: () -> (),
name: string,
}>
local DependsOn = jecs.component()
local Phase = jecs.tag()
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
local pair = jecs.pair
local types = require(ReplicatedStorage.types)
local function ECS_PHASE(world, after: types.Entity)
local phase = world:entity()
world:add(phase, Phase)
if after then
local dependency = pair(DependsOn, after)
world:add(phase, dependency)
end
return phase
end
local Heartbeat = jecs.tag()
jecs.meta(Heartbeat, Phase)
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
local PreSimulation = jecs.tag()
jecs.meta(PreSimulation, Phase)
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
local PreAnimation = jecs.tag()
jecs.meta(PreAnimation, Phase)
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
local PreRender = jecs.tag()
jecs.meta(PreRender, Phase)
jecs.meta(PreRender, Event, RunService.PreRender)
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
local system = world:entity()
local p = phase or Heartbeat
local fn = require(mod) :: (...any) -> ()
world:set(system, System, {
fn = fn(world, 0) or fn,
name = mod.Name,
})
local depends_on = DependsOn :: jecs.Entity
world:add(system, pair(depends_on, p))
end
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
local phase_name = world:get(phase, jecs.Name) :: string
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = scheduler:register_system({
phase = phase_name,
name = s.name,
}),
fn = s.fn
})
end
for after in world:query(Phase, pair(DependsOn, phase)) do
find_systems_w_phase(world, systems, after)
end
return systems
end
local function ECS_RUN(world: types.World)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
if RunService:IsClient() then
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
end
for phase, event in world:query(Event, Phase) do
local systems = find_systems_w_phase(world, {}, phase)
event:Connect(function(...)
for _, system in systems do
scheduler:run(system.id, system.fn, world, ...)
end
end)
end
end
return {
PHASE = ECS_PHASE,
SYSTEM = ECS_SYSTEM,
RUN = ECS_RUN,
phases = {
Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation,
PreRender = PreRender
},
components = {
System = System,
DependsOn = DependsOn,
Phase = Phase,
Event = Event,
}
}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
jabby.set_check_function(function() return true end)
local scheduler = jabby.scheduler.create("jabby scheduler")
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = scheduler,
},
})
local ContextActionService = game:GetService("ContextActionService")
local function create_widget(_, state: Enum.UserInputState)
local client = jabby.obtain_client()
if state ~= Enum.UserInputState.Begin then return end
client.spawn_app(client.apps.home, nil)
end
local RunService = game:GetService("RunService")
local System = jecs.component() :: jecs.Id<{
fn: () -> (),
name: string,
}>
local DependsOn = jecs.component()
local Phase = jecs.tag()
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
local pair = jecs.pair
local types = require(ReplicatedStorage.types)
local function ECS_PHASE(world, after: types.Entity)
local phase = world:entity()
world:add(phase, Phase)
if after then
local dependency = pair(DependsOn, after)
world:add(phase, dependency)
end
return phase
end
local Heartbeat = jecs.tag()
jecs.meta(Heartbeat, Phase)
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
local PreSimulation = jecs.tag()
jecs.meta(PreSimulation, Phase)
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
local PreAnimation = jecs.tag()
jecs.meta(PreAnimation, Phase)
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
local PreRender = jecs.tag()
jecs.meta(PreRender, Phase)
jecs.meta(PreRender, Event, RunService.PreRender)
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
local system = world:entity()
local p = phase or Heartbeat
local fn = require(mod) :: (...any) -> ()
world:set(system, System, {
fn = fn(world, 0) or fn,
name = mod.Name,
})
local depends_on = DependsOn :: jecs.Entity
world:add(system, pair(depends_on, p))
end
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
local phase_name = world:get(phase, jecs.Name) :: string
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = scheduler:register_system({
phase = phase_name,
name = s.name,
}),
fn = s.fn
})
end
for after in world:query(Phase, pair(DependsOn, phase)) do
find_systems_w_phase(world, systems, after)
end
return systems
end
local function ECS_RUN(world: types.World)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
if RunService:IsClient() then
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
end
for phase, event in world:query(Event, Phase) do
local systems = find_systems_w_phase(world, {}, phase)
event:Connect(function(...)
for _, system in systems do
scheduler:run(system.id, system.fn, world, ...)
end
end)
end
end
return {
PHASE = ECS_PHASE,
SYSTEM = ECS_SYSTEM,
RUN = ECS_RUN,
phases = {
Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation,
PreRender = PreRender
},
components = {
System = System,
DependsOn = DependsOn,
Phase = Phase,
Event = Event,
}
}

View file

@ -1,71 +1,86 @@
local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes")
local collect = require("../collect")
local client_ids = {}
local function ecs_map_get(world: types.World, id: types.Entity)
local deserialised_id = client_ids[id]
if not deserialised_id then
if world:has(id, jecs.Name) then
deserialised_id = world:entity(id)
else
if world:exists(id) then
deserialised_id = world:entity()
else
deserialised_id = world:entity(id)
end
end
client_ids[id] = deserialised_id
end
return deserialised_id
end
local function ecs_make_alive_id(world: types.World, id: jecs.Id)
local rel = jecs.ECS_PAIR_FIRST(id)
local tgt = jecs.ECS_PAIR_SECOND(id)
ecs_map_get(world, rel)
ecs_map_get(world, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World)
return function()
for snapshot in snapshots do
for key, map in snapshot do
local id = (tonumber(key) :: any) :: jecs.Id
if jecs.IS_PAIR(id) then
ecs_make_alive_id(world, id)
end
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_map_get(world, entity)
world:add(entity, id)
end
else
local values = map.values :: { any }
for i, entity in set do
entity = ecs_map_get(world, entity)
world:set(entity, id, values[i])
end
end
end
local removed = map.removed
if removed then
for i, e in removed do
if not world:contains(e) then
continue
end
world:remove(e, id)
end
end
end
end
end
end
local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes")
local collect = require("../collect")
local client_ids = {}
local function ecs_map_get(world, id)
local deserialised_id = client_ids[id]
if not deserialised_id then
if world:has(id, jecs.Name) then
deserialised_id = world:entity(id)
else
deserialised_id = world:entity()
end
client_ids[id] = deserialised_id
end
-- local deserialised_id = client_ids[id]
-- if not deserialised_id then
-- if world:has(id, jecs.Name) then
-- deserialised_id = world:entity(id)
-- else
-- if world:exists(id) then
-- deserialised_id = world:entity()
-- else
-- deserialised_id = world:entity(id)
-- end
-- end
-- client_ids[id] = deserialised_id
-- end
return deserialised_id
end
local function ecs_make_alive_id(world, id)
local rel = jecs.ECS_PAIR_FIRST(id)
local tgt = jecs.ECS_PAIR_SECOND(id)
rel = ecs_map_get(world, rel)
tgt = ecs_map_get(world, tgt)
return jecs.pair(rel, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World)
for snapshot in snapshots do
for id, map in snapshot do
id = tonumber(id)
if jecs.IS_PAIR(id) then
id = ecs_make_alive_id(world, id)
end
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_map_get(world, entity)
world:add(entity, id)
end
else
local values = map.values
for i, entity in set do
entity = ecs_map_get(world, entity)
world:set(entity, id, values[i])
end
end
end
local removed = map.removed
if removed then
for i, e in removed do
if not world:contains(e) then
continue
end
world:remove(e, id)
end
end
end
end
end

View file

@ -1,8 +1,15 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local observers_add = require("../ReplicatedStorage/observers_add")
export type World = typeof(observers_add(jecs.world()))
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
return {}
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local observers_add = require("../ReplicatedStorage/observers_add")
export type World = typeof(observers_add(jecs.world()))
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
export type Snapshot = {
[string]: {
set: { jecs.Entity }?,
values: { any }?,
removed: { jecs.Entity }?
}
}
return {}

View file

@ -1,12 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types)
return function(world: types.World, dt: number)
for e in world:query(ct.Player):without(ct.Health) do
world:set(e, ct.Health, 100)
end
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
world:set(e, ct.Poison, 10)
end
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types)
return function(world: types.World, dt: number)
for e in world:query(ct.Player):without(ct.Health) do
world:set(e, ct.Health, 100)
end
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
world:set(e, ct.Poison, 10)
end
end

View file

@ -1,20 +1,20 @@
local collect = require("../../ReplicatedStorage/collect")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local Players = game:GetService("Players")
local player_added = collect(Players.PlayerAdded)
return function(world: types.World, dt: number)
for player in player_added do
local entity = world:entity()
world:set(entity, ct.Player, player)
end
for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character
if character then
if not character.Parent then
world:set(entity, ct.Renderable, character)
end
end
end
local collect = require("../../ReplicatedStorage/collect")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local Players = game:GetService("Players")
local player_added = collect(Players.PlayerAdded)
return function(world: types.World, dt: number)
for player in player_added do
local entity = world:entity()
world:set(entity, ct.Player, player)
end
for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character
if character then
if not character.Parent then
world:set(entity, ct.Renderable, character)
end
end
end

View file

@ -1,12 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
return function(world, dt)
for e, poison, health in world:query(ct.Poison, ct.Health) do
local health_after_tick = health - poison * dt * 0.05
if health_after_tick < 0 then
world:remove(e, ct.Health)
continue
end
world:set(e, ct.Health, health_after_tick)
end
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
return function(world, dt)
for e, poison, health in world:query(ct.Poison, ct.Health) do
local health_after_tick = health - poison * dt * 0.05
if health_after_tick < 0 then
world:remove(e, ct.Health)
continue
end
world:set(e, ct.Health, health_after_tick)
end
end

View file

@ -1,122 +1,190 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes")
return function(world: types.World)
local storages = {}
for component in world:query(ct.Networked) do
local is_tag = jecs.is_tag(world, component)
local storage = {} :: { [types.Entity]: any }
storages[component] = storage
if is_tag then
world:added(component, function(entity)
storage[entity] = true
end)
else
world:added(component, function(entity, _, value)
storage[entity] = value
end)
world:changed(component, function(entity, _, value)
storage[entity] = value
end)
end
world:removed(component, function(entity)
storage[entity] = "jecs.Remove"
end)
end
for relation in world:query(ct.NetworkedPair) do
world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
if is_tag then
storage[entity] = true
else
storage[entity] = value
end
end)
world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
if is_tag then
return
end
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = value
end :: <T>(types.Entity, types.Id<T>, T) -> ())
world:removed(relation, function(entity, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = "jecs.Remove"
end)
end
return function()
local snapshot = {} :: {
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}
local set_ids = {} :: { types.Entity }
local removed_ids = {} :: { types.Entity }
for component, storage in storages do
local set_values = {}
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
if dirty then
snapshot[tostring(component)] = {
set = if set_n > 0 then table.move(set_ids, 1, set_n, 1, {}) else nil,
values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then table.move(removed_ids, 1, removed_n, 1, {} :: { types.Entity }) else nil
} :: any
end
end
if next(snapshot) ~= nil then
remotes.replication:FireAllClients(snapshot)
end
end
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes")
local components = ct :: {[string]: jecs.Entity }
return function(world: ty.World)
--- integration test
-- for _ = 1, 10 do
-- local e = world:entity()
-- world:set(e, ct.TestA, true)
-- end
local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }}
local networked_components = {}
local networked_pairs = {}
for component in world:each(ct.Networked) do
local name = world:get(component, jecs.Name) :: string
if components[name] == nil then
continue
end
storages[component] = {}
table.insert(networked_components, component)
end
for relation in world:each(ct.NetworkedPair) do
local name = world:get(relation, jecs.Name) :: string
if not components[name] then
continue
end
table.insert(networked_pairs, relation)
end
for _, component in networked_components do
local name = world:get(component, jecs.Name) :: string
if not components[name] then
error(`Networked Component (%id{component}%name{name})`)
end
local is_tag = jecs.is_tag(world, component)
local storage = storages[component]
if is_tag then
world:added(component, function(entity)
storage[entity] = true
end)
else
world:added(component, function(entity, _, value)
storage[entity] = value
end)
world:changed(component, function(entity, _, value)
storage[entity] = value
end)
end
world:removed(component, function(entity)
storage[entity] = "jecs.Remove"
end)
end
for _, relation in networked_pairs do
world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
if is_tag then
storage[entity] = true
else
storage[entity] = value
end
end)
world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
if is_tag then
return
end
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = value
end)
world:removed(relation, function(entity, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = "jecs.Remove"
end)
end
local players_added = collect(Players.PlayerAdded)
return function()
local snapshot_lazy: ty.Snapshot
local set_ids_lazy: { jecs.Entity }
for player in players_added do
if not snapshot_lazy then
snapshot_lazy, set_ids_lazy = {}, {}
for component, storage in storages do
local set_values = {}
local set_n = 0
local q = world:query(component)
local is_tag = jecs.is_tag(world, component)
for _, archetype in q:archetypes() do
local entities = archetype.entities
local entities_len = #entities
table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy)
if is_tag then
set_values = table.create(entities_len, true)
else
local column = archetype.columns[archetype.records[component]]
table.move(column, 1, entities_len, set_n + 1, set_values)
end
set_n += entities_len
end
local set = table.move(set_ids_lazy, 1, set_n, 1, {})
snapshot_lazy[tostring(component)] = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
}
end
end
remotes.replication:FireClient(player, snapshot_lazy)
end
local snapshot = {} :: ty.Snapshot
local set_ids = {}
local removed_ids = {}
for component, storage in storages do
local set_values = {} :: { any }
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif not world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
if dirty then
local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity }
local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity }
snapshot[tostring(component)] = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then removed else nil
}
end
end
if next(snapshot) ~= nil then
remotes.replication:FireAllClients(snapshot)
end
end
end

View file

@ -1,21 +1,21 @@
# Contribution Guidelines
Whether you found an issue, or want to make a change to jecs, we'd love to hear back from the community on what features you want or bugs you've run into.
There's a few different ways you can go about this.
## Creating an Issue
This is what you should be filing if you have a bug you want to report.
[Click here](https://github.com/Ukendio/jecs/issues/new/choose) to file a bug report. We have a few templates ready for the most common issue types.
Additionally, see the [Submitting Issues](../contributing/issues) page for more information.
## Creating a Pull Request
This is what you should be filing if you have a change you want to merge into the main project.
[Click here](https://github.com/Ukendio/jecs/compare) to select the branch you want to merge from.
Additionally, see the [Submitting Pull Requests](../contributing/pull-requests) page for more information.
# Contribution Guidelines
Whether you found an issue, or want to make a change to jecs, we'd love to hear back from the community on what features you want or bugs you've run into.
There's a few different ways you can go about this.
## Creating an Issue
This is what you should be filing if you have a bug you want to report.
[Click here](https://github.com/Ukendio/jecs/issues/new/choose) to file a bug report. We have a few templates ready for the most common issue types.
Additionally, see the [Submitting Issues](../contributing/issues) page for more information.
## Creating a Pull Request
This is what you should be filing if you have a change you want to merge into the main project.
[Click here](https://github.com/Ukendio/jecs/compare) to select the branch you want to merge from.
Additionally, see the [Submitting Pull Requests](../contributing/pull-requests) page for more information.

View file

@ -1,24 +1,24 @@
# Submitting Issues
When you're submitting an issue, generally they fall into a few categories:
## Bug
We need some information to figure out what's going wrong. At a minimum, you need to tell us:
(1) What's supposed to happen
(2) What actually happened
(3) Steps to reproduce
Stack traces and other useful information that you find make a bug report more likely to be fixed.
Consult the template for a bug report if you don't know or have questions about how to format this.
## Documentation
Depending on how you go about it, this can be done as a [Pull Request](../contributing/pull-requests) instead of an issue. Generally, we need to know what was wrong, what you changed, and how it improved the documentation if it isn't obvious.
We just need to know what's wrong. You should fill out a [PR](../contributing/pull-requests) if you know what should be there instead.
# Submitting Issues
When you're submitting an issue, generally they fall into a few categories:
## Bug
We need some information to figure out what's going wrong. At a minimum, you need to tell us:
(1) What's supposed to happen
(2) What actually happened
(3) Steps to reproduce
Stack traces and other useful information that you find make a bug report more likely to be fixed.
Consult the template for a bug report if you don't know or have questions about how to format this.
## Documentation
Depending on how you go about it, this can be done as a [Pull Request](../contributing/pull-requests) instead of an issue. Generally, we need to know what was wrong, what you changed, and how it improved the documentation if it isn't obvious.
We just need to know what's wrong. You should fill out a [PR](../contributing/pull-requests) if you know what should be there instead.

View file

@ -1,77 +1,77 @@
# Submitting Pull Requests
When submitting a Pull Request, there's a few reasons to do so:
## Documentation
If there's something to change with the documentation, you should follow a similar format to this example:
An example of an appropriate typo-fixing PR would be:
>**Brief Description of your Changes**
>
>I fixed a couple of typos found in the /contributing/issues.md file.
>
>**Impact of your Changes**
>
>- Documentation is more clear and readable for the users.
>
>**Tests Performed**
>
>Ran `vitepress dev docs` and verified it was built successfully.
>
>**Additional Comments**
>
>[At Discretion]
## Change in Behavior
An example of an appropriate PR that adds a new feature would be:
>
>**Brief Description of your Changes**
>
>I added `jecs.best_function`, which gives everyone who uses the module an immediate boost in concurrent player counts. (this is a joke)
>
>**Impact of your Changes**
>
>- jecs functionality is extended to better fit the needs of the community [explain why].
>
>**Tests Performed**
>
>Added a few test cases to ensure the function runs as expected [link to changes].
>
>**Additional Comments**
>
>[At Discretion]
## Addons
If you made something you think should be included into the [resources page](../../resources), let us know!
We have tons of examples of libraries and other tools which can be used in conjunction with jecs on this page.
One example of a PR that would be accepted is:
>**Brief Description of your Changes**
>
>I added `jecs observers` to the addons page.
>
>**Impact of your Changes**
>
>- jecs observers are a different and important way of handling queries which benefit the users of jecs by [explain why your tool benefits users here]
>
>- [talk about why you went with this design instead of maybe an alternative]
>
>**Tests Performed**
>
> I used this tool in conjunction with jecs and ensured it works as expected.
>
> [If you wrote unit tests for your tool, mention it here.]
>
>**Additional Comments**
>
>[At Discretion]
Keep in mind the list on the addons page is *not* exhaustive. If you came up with a tool that doesn't fit into any of the categories listed, we still want to hear from you!
# Submitting Pull Requests
When submitting a Pull Request, there's a few reasons to do so:
## Documentation
If there's something to change with the documentation, you should follow a similar format to this example:
An example of an appropriate typo-fixing PR would be:
>**Brief Description of your Changes**
>
>I fixed a couple of typos found in the /contributing/issues.md file.
>
>**Impact of your Changes**
>
>- Documentation is more clear and readable for the users.
>
>**Tests Performed**
>
>Ran `vitepress dev docs` and verified it was built successfully.
>
>**Additional Comments**
>
>[At Discretion]
## Change in Behavior
An example of an appropriate PR that adds a new feature would be:
>
>**Brief Description of your Changes**
>
>I added `jecs.best_function`, which gives everyone who uses the module an immediate boost in concurrent player counts. (this is a joke)
>
>**Impact of your Changes**
>
>- jecs functionality is extended to better fit the needs of the community [explain why].
>
>**Tests Performed**
>
>Added a few test cases to ensure the function runs as expected [link to changes].
>
>**Additional Comments**
>
>[At Discretion]
## Addons
If you made something you think should be included into the [resources page](../../resources), let us know!
We have tons of examples of libraries and other tools which can be used in conjunction with jecs on this page.
One example of a PR that would be accepted is:
>**Brief Description of your Changes**
>
>I added `jecs observers` to the addons page.
>
>**Impact of your Changes**
>
>- jecs observers are a different and important way of handling queries which benefit the users of jecs by [explain why your tool benefits users here]
>
>- [talk about why you went with this design instead of maybe an alternative]
>
>**Tests Performed**
>
> I used this tool in conjunction with jecs and ensured it works as expected.
>
> [If you wrote unit tests for your tool, mention it here.]
>
>**Additional Comments**
>
>[At Discretion]
Keep in mind the list on the addons page is *not* exhaustive. If you came up with a tool that doesn't fit into any of the categories listed, we still want to hear from you!

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
--!optimize 2
--!native
--!strict
@ -117,6 +118,7 @@ local ECS_ID_MASK = 0b00
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48
local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false)
@ -168,7 +170,6 @@ end
local function ECS_COMBINE(id: number, generation: number): i53
return id + (generation * ECS_ENTITY_MASK)
end
local ECS_PAIR_OFFSET = 2^48
local function ECS_IS_PAIR(e: number): boolean
return e > ECS_PAIR_OFFSET
@ -2576,40 +2577,40 @@ export type World = {
component: <T>(self: World) -> Entity<T>,
--- 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: <T>(self: World, id: Entity, relation: Id<T>, index: number?) -> Entity?,
target: <T, a>(self: World, id: Entity<T>, relation: Id<a>, index: number?) -> Entity?,
--- Deletes an entity and all it's related components and relationships.
delete: (self: World, id: Entity) -> (),
delete: <T>(self: World, id: Entity<T>) -> (),
--- Adds a component to the entity with no value
add: <T>(self: World, id: Entity, component: Id<T>) -> (),
add: <T, a>(self: World, id: Entity<T>, component: Id<a>) -> (),
--- Assigns a value to a component on the given entity
set: <T>(self: World, id: Entity, component: Id<T>, data: T) -> (),
set: <T, a>(self: World, id: Entity<T>, component: Id<a>, data: a) -> (),
cleanup: (self: World) -> (),
-- Clears an entity from the world
clear: <T>(self: World, id: Id<T>) -> (),
clear: <a>(self: World, id: Id<a>) -> (),
--- Removes a component from the given entity
remove: <T>(self: World, id: Entity, component: Id<T>) -> (),
remove: <T, a>(self: World, id: Entity<T>, component: Id<a>) -> (),
--- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(self: World, id: Entity, Id<A>) -> A?)
& (<A, B>(self: World, id: Entity, Id<A>, Id<B>) -> (A?, B?))
& (<A, B, C>(self: World, id: Entity, Id<A>, Id<B>, Id<C>) -> (A?, B?, C?))
& <A, B, C, D>(self: World, id: Entity, Id<A>, Id<B>, Id<C>, Id<D>) -> (A?, B?, C?, D?),
get: & (<T, a>(World, Entity<T>, Id<a>) -> a?)
& (<T, a, b>(World, Entity<T>, Id<a>, Id<b>) -> (a?, b?))
& (<T, a, b, c>(World, Entity<T>, Id<a>, Id<b>, Id<c>) -> (a?, b?, c?))
& (<T, a, b, c, d>(World, Entity<T>, Id<a>, Id<b>, Id<c>, Id<d>) -> (a?, b?, c?, d?)),
--- Returns whether the entity has the ID.
has: (<A>(World, Entity, A) -> boolean)
& (<A, B>(World, Entity, A, B) -> boolean)
& (<A, B, C>(World, Entity, A, B, C) -> boolean)
& <A, B, C, D>(World, Entity, A, B, C, D) -> boolean,
has: (<T>(World, Entity<T>, Id) -> boolean)
& (<T>(World, Entity<T>, Id, Id) -> boolean)
& (<T>(World, Entity<T>, Id, Id, Id) -> boolean)
& <T>(World, Entity<T>, Id, Id, Id, Id) -> boolean,
--- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil.
parent:(self: World, entity: Entity) -> Entity,
parent: <T>(self: World, entity: Entity<T>) -> Entity,
--- Checks if the world contains the given entity
contains:(self: World, entity: Entity) -> boolean,
contains: <T>(self: World, entity: Entity<T>) -> boolean,
--- Checks if the entity exists
exists: (self: World, entity: Entity) -> boolean,
exists: <T>(self: World, entity: Entity<T>) -> boolean,
each: <T>(self: World, id: Id<T>) -> () -> Entity,

View file

@ -1,158 +1,158 @@
local c = {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,
white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,
green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,
red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,
yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}
local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
type i53 = number
type i24 = number
local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end
local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end
local ECS_ID = ECS_ENTITY_T_LO
local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end
local function ECS_GENERATION_INC(e: i53)
if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK
local next_gen = generation + 1
if next_gen > ECS_GENERATION_MASK then
return id
end
return ECS_COMBINE(id, next_gen) + flags
end
return ECS_COMBINE(e, 1)
end
local function bl()
print("")
end
local function pe(e)
local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
end
local function dprint(tbl: { [number]: number })
bl()
print("--------")
for i, e in tbl do
print("| "..pe(e).." |")
print("--------")
end
bl()
end
local max_id = 0
local alive_count = 0
local dense = {}
local sparse = {}
local function alloc()
if alive_count ~= #dense then
alive_count += 1
print("*recycled", pe(dense[alive_count]))
return dense[alive_count]
end
max_id += 1
local id = max_id
alive_count += 1
dense[alive_count] = id
sparse[id] = {
dense = alive_count
}
print("*allocated", pe(id))
return id
end
local function remove(entity)
local id = ECS_ID(entity)
local r = sparse[id]
local index_of_deleted_entity = r.dense
local last_entity_alive_at_index = alive_count -- last entity alive
alive_count -= 1
local last_alive_entity = dense[last_entity_alive_at_index]
local r_swap = sparse[ECS_ID(last_alive_entity)]
r_swap.dense = r.dense
r.dense = last_entity_alive_at_index
dense[index_of_deleted_entity] = last_alive_entity
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
print("*dellocated", pe(id))
end
local function alive(e)
local r = sparse[ECS_ID(e)]
return dense[r.dense] == e
end
local function pa(e)
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
end
local tprint = require("@testkit").print
local e1v0 = alloc()
local e2v0 = alloc()
local e3v0 = alloc()
local e4v0 = alloc()
local e5v0 = alloc()
pa(e1v0)
pa(e4v0)
remove(e5v0)
pa(e5v0)
local e5v1 = alloc()
pa(e5v0)
pa(e5v1)
pa(e2v0)
print(ECS_ID(e2v0))
dprint(dense)
remove(e2v0)
dprint(dense)
local c = {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,
white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,
green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,
red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,
yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,
red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,
green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,
gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}
local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
type i53 = number
type i24 = number
local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end
local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end
local ECS_ID = ECS_ENTITY_T_LO
local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end
local function ECS_GENERATION_INC(e: i53)
if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK
local next_gen = generation + 1
if next_gen > ECS_GENERATION_MASK then
return id
end
return ECS_COMBINE(id, next_gen) + flags
end
return ECS_COMBINE(e, 1)
end
local function bl()
print("")
end
local function pe(e)
local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
end
local function dprint(tbl: { [number]: number })
bl()
print("--------")
for i, e in tbl do
print("| "..pe(e).." |")
print("--------")
end
bl()
end
local max_id = 0
local alive_count = 0
local dense = {}
local sparse = {}
local function alloc()
if alive_count ~= #dense then
alive_count += 1
print("*recycled", pe(dense[alive_count]))
return dense[alive_count]
end
max_id += 1
local id = max_id
alive_count += 1
dense[alive_count] = id
sparse[id] = {
dense = alive_count
}
print("*allocated", pe(id))
return id
end
local function remove(entity)
local id = ECS_ID(entity)
local r = sparse[id]
local index_of_deleted_entity = r.dense
local last_entity_alive_at_index = alive_count -- last entity alive
alive_count -= 1
local last_alive_entity = dense[last_entity_alive_at_index]
local r_swap = sparse[ECS_ID(last_alive_entity)]
r_swap.dense = r.dense
r.dense = last_entity_alive_at_index
dense[index_of_deleted_entity] = last_alive_entity
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
print("*dellocated", pe(id))
end
local function alive(e)
local r = sparse[ECS_ID(e)]
return dense[r.dense] == e
end
local function pa(e)
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
end
local tprint = require("@testkit").print
local e1v0 = alloc()
local e2v0 = alloc()
local e3v0 = alloc()
local e4v0 = alloc()
local e5v0 = alloc()
pa(e1v0)
pa(e4v0)
remove(e5v0)
pa(e5v0)
local e5v1 = alloc()
pa(e5v0)
pa(e5v1)
pa(e2v0)
print(ECS_ID(e2v0))
dprint(dense)
remove(e2v0)
dprint(dense)

View file

@ -1,122 +1,122 @@
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
_G.__JECS_HI_COMPONENT_ID = 300
local ecs = require(ReplicatedStorage.ecs)
-- 500 entities
-- 2-30 components on each entity
-- 300 unique components
-- 200 systems
-- 1-10 components to query per system
local startTime = os.clock()
local world = ecs.World.new()
local components = {}
for i = 1, 300 do -- 300 components
components[i] = world:component()
end
local archetypes = {}
for i = 1, 50 do -- 50 archetypes
local archetype = {}
for _ = 1, math.random(2, 30) do
local componentId = math.random(1, #components)
table.insert(archetype, components[componentId])
end
archetypes[i] = archetype
end
for _ = 1, 1000 do -- 1000 entities in the world
local componentsToAdd = {}
local archetypeId = math.random(1, #archetypes)
local e = world:entity()
for _, component in ipairs(archetypes[archetypeId]) do
world:set(e, component, {
DummyData = math.random(1, 5000),
})
end
end
local function values(t)
local array = {}
for _, v in t do
table.insert(array, v)
end
return array
end
local contiguousComponents = values(components)
local systemComponentsToQuery = {}
for _ = 1, 200 do -- 200 systems
local numComponentsToQuery = math.random(1, 10)
local componentsToQuery = {}
for _ = 1, numComponentsToQuery do
table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)])
end
table.insert(systemComponentsToQuery, componentsToQuery)
end
local worldCreateTime = os.clock() - startTime
local results = {}
startTime = os.clock()
RunService.Heartbeat:Connect(function()
local added = 0
local systemStartTime = os.clock()
debug.profilebegin("systems")
for _, componentsToQuery in ipairs(systemComponentsToQuery) do
debug.profilebegin("system")
for entityId, firstComponent in world:query(unpack(componentsToQuery)) do
world:set(
entityId,
{
DummyData = firstComponent.DummyData + 1,
}
)
added += 1
end
debug.profileend()
end
debug.profileend()
if os.clock() - startTime < 4 then
-- discard first 4 seconds
return
end
if results == nil then
return
elseif #results < 1000 then
table.insert(results, os.clock() - systemStartTime)
else
print("added", added)
print("World created in", worldCreateTime * 1000, "ms")
local sum = 0
for _, result in ipairs(results) do
sum += result
end
print(("Average frame time: %fms"):format((sum / #results) * 1000))
results = nil
local n = #world.archetypes
print(
("X entities\n%d components\n%d systems\n%d archetypes"):format(
#components,
#systemComponentsToQuery,
n
)
)
end
end)
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
_G.__JECS_HI_COMPONENT_ID = 300
local ecs = require(ReplicatedStorage.ecs)
-- 500 entities
-- 2-30 components on each entity
-- 300 unique components
-- 200 systems
-- 1-10 components to query per system
local startTime = os.clock()
local world = ecs.World.new()
local components = {}
for i = 1, 300 do -- 300 components
components[i] = world:component()
end
local archetypes = {}
for i = 1, 50 do -- 50 archetypes
local archetype = {}
for _ = 1, math.random(2, 30) do
local componentId = math.random(1, #components)
table.insert(archetype, components[componentId])
end
archetypes[i] = archetype
end
for _ = 1, 1000 do -- 1000 entities in the world
local componentsToAdd = {}
local archetypeId = math.random(1, #archetypes)
local e = world:entity()
for _, component in ipairs(archetypes[archetypeId]) do
world:set(e, component, {
DummyData = math.random(1, 5000),
})
end
end
local function values(t)
local array = {}
for _, v in t do
table.insert(array, v)
end
return array
end
local contiguousComponents = values(components)
local systemComponentsToQuery = {}
for _ = 1, 200 do -- 200 systems
local numComponentsToQuery = math.random(1, 10)
local componentsToQuery = {}
for _ = 1, numComponentsToQuery do
table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)])
end
table.insert(systemComponentsToQuery, componentsToQuery)
end
local worldCreateTime = os.clock() - startTime
local results = {}
startTime = os.clock()
RunService.Heartbeat:Connect(function()
local added = 0
local systemStartTime = os.clock()
debug.profilebegin("systems")
for _, componentsToQuery in ipairs(systemComponentsToQuery) do
debug.profilebegin("system")
for entityId, firstComponent in world:query(unpack(componentsToQuery)) do
world:set(
entityId,
{
DummyData = firstComponent.DummyData + 1,
}
)
added += 1
end
debug.profileend()
end
debug.profileend()
if os.clock() - startTime < 4 then
-- discard first 4 seconds
return
end
if results == nil then
return
elseif #results < 1000 then
table.insert(results, os.clock() - systemStartTime)
else
print("added", added)
print("World created in", worldCreateTime * 1000, "ms")
local sum = 0
for _, result in ipairs(results) do
sum += result
end
print(("Average frame time: %fms"):format((sum / #results) * 1000))
results = nil
local n = #world.archetypes
print(
("X entities\n%d components\n%d systems\n%d archetypes"):format(
#components,
#systemComponentsToQuery,
n
)
)
end
end)

View file

@ -1,24 +1,24 @@
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
local FriendsWith = world:component()
world:print_snapshot()
local e1 = world:entity()
local e2 = world:entity()
world:delete(e2)
world:print_snapshot()
local e3 = world:entity()
world:add(e3, pair(ChildOf, e1))
local e4 = world:entity()
world:add(e4, pair(FriendsWith, e3))
world:print_snapshot()
world:delete(e1)
world:delete(e3)
world:print_snapshot()
world:print_entity_index()
world:entity()
world:entity()
world:print_snapshot()
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
local FriendsWith = world:component()
world:print_snapshot()
local e1 = world:entity()
local e2 = world:entity()
world:delete(e2)
world:print_snapshot()
local e3 = world:entity()
world:add(e3, pair(ChildOf, e1))
local e4 = world:entity()
world:add(e4, pair(FriendsWith, e3))
world:print_snapshot()
world:delete(e1)
world:delete(e3)
world:print_snapshot()
world:print_entity_index()
world:entity()
world:entity()
world:print_snapshot()

View file

@ -1,153 +1,153 @@
import os
LCOV_FILE = "coverage.out"
OUTPUT_DIR = "coverage"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def parse_lcov(content):
"""Parses LCOV data from a single string."""
files = {}
current_file = None
for line in content.splitlines():
if line.startswith("SF:"):
current_file = line[3:].strip()
files[current_file] = {"coverage": {}, "functions": []}
elif line.startswith("DA:") and current_file:
parts = line[3:].split(",")
line_num = int(parts[0])
execution_count = int(parts[1])
files[current_file]["coverage"][line_num] = execution_count
elif line.startswith("FN:") and current_file:
parts = line[3:].split(",")
line_num = int(parts[0])
function_name = parts[1].strip()
files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0})
elif line.startswith("FNDA:") and current_file:
parts = line[5:].split(",")
hit_count = int(parts[0])
function_name = parts[1].strip()
for func in files[current_file]["functions"]:
if func["name"] == function_name:
func["hits"] = hit_count
break
return files
def read_source_file(filepath):
"""Reads source file content if available."""
if not os.path.exists(filepath):
return []
with open(filepath, "r", encoding="utf-8") as f:
return f.readlines()
def generate_file_html(filepath, coverage_data, functions_data):
"""Generates an HTML file for a specific source file."""
filename = os.path.basename(filepath)
source_code = read_source_file(filepath)
html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
total_hits = sum(func["hits"] for func in functions_data)
max_hits = max((func["hits"] for func in functions_data), default=0)
total_functions = len(functions_data)
covered_functions = sum(1 for func in functions_data if func["hits"] > 0)
function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0
lines = [
"<html><head>",
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
'<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>',
"<style>",
"body { font-family: monospace; text-align: center; }",
"#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }",
"#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }",
"#funcTable th { background-color: #ddd; }",
"#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }",
".zero-hits { background-color: #fcc; font-weight: bold; color: red; }",
".nonzero-hits { color: green; font-weight: bold; }",
".low-hits { background-color: #ffe6b3; }",
".high-hits { background-color: #cfc; }",
".source-code-table { margin-left: 10px; }"
"th, td { padding: 0px; font-size: 12px; }",
"table.table { font-size: 14px; border-collapse: collapse; }",
"table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }",
"table.table tr { height: auto; }",
"</style></head><body>",
f'<h1 class="text-center">{filename} Coverage</h1>',
f'<h2>Total Execution Hits: {total_hits}</h2>',
f'<h2>Function Coverage Overview: {function_coverage_percent:.2f}%</h2>',
'<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">'
'Toggle Function Coverage</button>',
'<div class="collapse show" id="funcTable">',
'<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>'
]
longest_name = max((len(func["name"]) for func in functions_data), default=0)
for func in functions_data:
hit_color = "red" if func["hits"] == 0 else "green"
lines.append(
f'<tr><td style="padding: 1px; min-width: {longest_name}ch;">{func["name"]}</td>'
f'<td style="padding: 1px; color: {hit_color}; font-weight: bold;">{func["hits"]}</td></tr>'
)
lines.append('</tbody></table></div>') # Close collapsible div
lines.append('<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>')
for i, line in enumerate(source_code, start=1):
stripped_line = line.strip()
class_name = "text-muted"
if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"):
count_display = "<span class='text-muted'>N/A</span>"
lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{line.strip()}</td>></tr>')
else:
count = coverage_data.get(i, 0)
class_name = "zero-hits" if count == 0 else "low-hits" if count < max_hits * 0.3 else "high-hits"
count_display = f'{count}'
marked_text = f'<span class={class_name}>{line.strip()}</span>'
lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{marked_text}</td></tr>')
lines.append("</tbody></table></body></html>")
with open(html_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
def generate_index(files):
"""Generates an index.html summarizing the coverage."""
index_html = [
"<html><head>",
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
"</head><body>",
'<h1 class="text-center">Coverage Report</h1>',
'<table class="table table-striped table-bordered"><thead><tr><th>File</th><th>Total Hits</th><th>Functions</th></tr></thead><tbody>'
]
for filepath, data in files.items():
filename = os.path.basename(filepath)
total_hits = sum(func["hits"] for func in data["functions"])
total_functions = len(data["functions"])
index_html.append(f'<tr><td><a href="{filename}.html">{filename}</a></td><td>{total_hits}</td><td>{total_functions}</td></tr>')
index_html.append("</tbody></table></body></html>")
with open(os.path.join(OUTPUT_DIR, "index.html"), "w", encoding="utf-8") as f:
f.write("\n".join(index_html))
with open(LCOV_FILE, "r", encoding="utf-8") as f:
lcov_content = f.read()
files_data = parse_lcov(lcov_content)
for file_path, data in files_data.items():
generate_file_html(file_path, data["coverage"], data["functions"])
generate_index(files_data)
print(f"Coverage report generated in {OUTPUT_DIR}/index.html")
import os
LCOV_FILE = "coverage.out"
OUTPUT_DIR = "coverage"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def parse_lcov(content):
"""Parses LCOV data from a single string."""
files = {}
current_file = None
for line in content.splitlines():
if line.startswith("SF:"):
current_file = line[3:].strip()
files[current_file] = {"coverage": {}, "functions": []}
elif line.startswith("DA:") and current_file:
parts = line[3:].split(",")
line_num = int(parts[0])
execution_count = int(parts[1])
files[current_file]["coverage"][line_num] = execution_count
elif line.startswith("FN:") and current_file:
parts = line[3:].split(",")
line_num = int(parts[0])
function_name = parts[1].strip()
files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0})
elif line.startswith("FNDA:") and current_file:
parts = line[5:].split(",")
hit_count = int(parts[0])
function_name = parts[1].strip()
for func in files[current_file]["functions"]:
if func["name"] == function_name:
func["hits"] = hit_count
break
return files
def read_source_file(filepath):
"""Reads source file content if available."""
if not os.path.exists(filepath):
return []
with open(filepath, "r", encoding="utf-8") as f:
return f.readlines()
def generate_file_html(filepath, coverage_data, functions_data):
"""Generates an HTML file for a specific source file."""
filename = os.path.basename(filepath)
source_code = read_source_file(filepath)
html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
total_hits = sum(func["hits"] for func in functions_data)
max_hits = max((func["hits"] for func in functions_data), default=0)
total_functions = len(functions_data)
covered_functions = sum(1 for func in functions_data if func["hits"] > 0)
function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0
lines = [
"<html><head>",
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
'<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>',
"<style>",
"body { font-family: monospace; text-align: center; }",
"#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }",
"#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }",
"#funcTable th { background-color: #ddd; }",
"#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }",
".zero-hits { background-color: #fcc; font-weight: bold; color: red; }",
".nonzero-hits { color: green; font-weight: bold; }",
".low-hits { background-color: #ffe6b3; }",
".high-hits { background-color: #cfc; }",
".source-code-table { margin-left: 10px; }"
"th, td { padding: 0px; font-size: 12px; }",
"table.table { font-size: 14px; border-collapse: collapse; }",
"table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }",
"table.table tr { height: auto; }",
"</style></head><body>",
f'<h1 class="text-center">{filename} Coverage</h1>',
f'<h2>Total Execution Hits: {total_hits}</h2>',
f'<h2>Function Coverage Overview: {function_coverage_percent:.2f}%</h2>',
'<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">'
'Toggle Function Coverage</button>',
'<div class="collapse show" id="funcTable">',
'<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>'
]
longest_name = max((len(func["name"]) for func in functions_data), default=0)
for func in functions_data:
hit_color = "red" if func["hits"] == 0 else "green"
lines.append(
f'<tr><td style="padding: 1px; min-width: {longest_name}ch;">{func["name"]}</td>'
f'<td style="padding: 1px; color: {hit_color}; font-weight: bold;">{func["hits"]}</td></tr>'
)
lines.append('</tbody></table></div>') # Close collapsible div
lines.append('<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>')
for i, line in enumerate(source_code, start=1):
stripped_line = line.strip()
class_name = "text-muted"
if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"):
count_display = "<span class='text-muted'>N/A</span>"
lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{line.strip()}</td>></tr>')
else:
count = coverage_data.get(i, 0)
class_name = "zero-hits" if count == 0 else "low-hits" if count < max_hits * 0.3 else "high-hits"
count_display = f'{count}'
marked_text = f'<span class={class_name}>{line.strip()}</span>'
lines.append(f'<tr><td>{i}</td><td>{count_display}</td><td>{marked_text}</td></tr>')
lines.append("</tbody></table></body></html>")
with open(html_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
def generate_index(files):
"""Generates an index.html summarizing the coverage."""
index_html = [
"<html><head>",
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">',
"</head><body>",
'<h1 class="text-center">Coverage Report</h1>',
'<table class="table table-striped table-bordered"><thead><tr><th>File</th><th>Total Hits</th><th>Functions</th></tr></thead><tbody>'
]
for filepath, data in files.items():
filename = os.path.basename(filepath)
total_hits = sum(func["hits"] for func in data["functions"])
total_functions = len(data["functions"])
index_html.append(f'<tr><td><a href="{filename}.html">{filename}</a></td><td>{total_hits}</td><td>{total_functions}</td></tr>')
index_html.append("</tbody></table></body></html>")
with open(os.path.join(OUTPUT_DIR, "index.html"), "w", encoding="utf-8") as f:
f.write("\n".join(index_html))
with open(LCOV_FILE, "r", encoding="utf-8") as f:
lcov_content = f.read()
files_data = parse_lcov(lcov_content)
for file_path, data in files_data.items():
generate_file_html(file_path, data["coverage"], data["functions"])
generate_index(files_data)
print(f"Coverage report generated in {OUTPUT_DIR}/index.html")