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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,122 +1,190 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types") local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components") local ct = require("../../ReplicatedStorage/components")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes") local remotes = require("../../ReplicatedStorage/remotes")
local components = ct :: {[string]: jecs.Entity }
return function(world: types.World)
local storages = {} return function(world: ty.World)
for component in world:query(ct.Networked) do --- integration test
local is_tag = jecs.is_tag(world, component)
local storage = {} :: { [types.Entity]: any } -- for _ = 1, 10 do
storages[component] = storage -- local e = world:entity()
-- world:set(e, ct.TestA, true)
if is_tag then -- end
world:added(component, function(entity)
storage[entity] = true local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }}
end) local networked_components = {}
else local networked_pairs = {}
world:added(component, function(entity, _, value)
storage[entity] = value for component in world:each(ct.Networked) do
end) local name = world:get(component, jecs.Name) :: string
world:changed(component, function(entity, _, value) if components[name] == nil then
storage[entity] = value continue
end) end
end
storages[component] = {}
world:removed(component, function(entity)
storage[entity] = "jecs.Remove" table.insert(networked_components, component)
end) end
end
for relation in world:each(ct.NetworkedPair) do
for relation in world:query(ct.NetworkedPair) do local name = world:get(relation, jecs.Name) :: string
world:added(relation, function(entity, id, value) if not components[name] then
local is_tag = jecs.is_tag(world, id) continue
local storage = storages[id] end
if not storage then table.insert(networked_pairs, relation)
storage = {} end
storages[id] = storage
end for _, component in networked_components do
if is_tag then local name = world:get(component, jecs.Name) :: string
storage[entity] = true if not components[name] then
else error(`Networked Component (%id{component}%name{name})`)
storage[entity] = value end
end local is_tag = jecs.is_tag(world, component)
end) local storage = storages[component]
if is_tag then
world:changed(relation, function(entity, id, value) world:added(component, function(entity)
local is_tag = jecs.is_tag(world, id) storage[entity] = true
if is_tag then end)
return else
end world:added(component, function(entity, _, value)
storage[entity] = value
local storage = storages[id] end)
if not storage then world:changed(component, function(entity, _, value)
storage = {} storage[entity] = value
storages[id] = storage end)
end end
storage[entity] = value world:removed(component, function(entity)
end :: <T>(types.Entity, types.Id<T>, T) -> ()) storage[entity] = "jecs.Remove"
end)
world:removed(relation, function(entity, id) end
local storage = storages[id]
if not storage then for _, relation in networked_pairs do
storage = {} world:added(relation, function(entity, id, value)
storages[id] = storage local is_tag = jecs.is_tag(world, id)
end local storage = storages[id]
if not storage then
storage[entity] = "jecs.Remove" storage = {}
end) storages[id] = storage
end end
if is_tag then
return function() storage[entity] = true
local snapshot = {} :: { else
[string]: { storage[entity] = value
set: { types.Entity }?, end
values: { any }?, end)
removed: { types.Entity }?
} world:changed(relation, function(entity, id, value)
} local is_tag = jecs.is_tag(world, id)
if is_tag then
local set_ids = {} :: { types.Entity } return
local removed_ids = {} :: { types.Entity } end
for component, storage in storages do local storage = storages[id]
local set_values = {} if not storage then
local set_n = 0 storage = {}
local removed_n = 0 storages[id] = storage
for e, v in storage do end
if v ~= "jecs.Remove" then
set_n += 1 storage[entity] = value
set_ids[set_n] = e end)
set_values[set_n] = v or true
elseif world:contains(e) then world:removed(relation, function(entity, id)
removed_n += 1 local storage = storages[id]
removed_ids[removed_n] = e if not storage then
end storage = {}
end storages[id] = storage
end
table.clear(storage)
storage[entity] = "jecs.Remove"
local dirty = false end)
end
if set_n > 0 or removed_n > 0 then
dirty = true local players_added = collect(Players.PlayerAdded)
end
return function()
if dirty then local snapshot_lazy: ty.Snapshot
snapshot[tostring(component)] = { local set_ids_lazy: { jecs.Entity }
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, for player in players_added do
removed = if removed_n > 0 then table.move(removed_ids, 1, removed_n, 1, {} :: { types.Entity }) else nil if not snapshot_lazy then
} :: any snapshot_lazy, set_ids_lazy = {}, {}
end
end for component, storage in storages do
local set_values = {}
if next(snapshot) ~= nil then local set_n = 0
remotes.replication:FireAllClients(snapshot)
end local q = world:query(component)
end local is_tag = jecs.is_tag(world, component)
end 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 # 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. 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. There's a few different ways you can go about this.
## Creating an Issue ## Creating an Issue
This is what you should be filing if you have a bug you want to report. 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. [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. Additionally, see the [Submitting Issues](../contributing/issues) page for more information.
## Creating a Pull Request ## Creating a Pull Request
This is what you should be filing if you have a change you want to merge into the main project. 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. [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. Additionally, see the [Submitting Pull Requests](../contributing/pull-requests) page for more information.

View file

@ -1,24 +1,24 @@
# Submitting Issues # Submitting Issues
When you're submitting an issue, generally they fall into a few categories: When you're submitting an issue, generally they fall into a few categories:
## Bug ## Bug
We need some information to figure out what's going wrong. At a minimum, you need to tell us: 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 (1) What's supposed to happen
(2) What actually happened (2) What actually happened
(3) Steps to reproduce (3) Steps to reproduce
Stack traces and other useful information that you find make a bug report more likely to be fixed. 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. Consult the template for a bug report if you don't know or have questions about how to format this.
## Documentation ## 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. 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. 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 # Submitting Pull Requests
When submitting a Pull Request, there's a few reasons to do so: When submitting a Pull Request, there's a few reasons to do so:
## Documentation ## Documentation
If there's something to change with the documentation, you should follow a similar format to this example: 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: An example of an appropriate typo-fixing PR would be:
>**Brief Description of your Changes** >**Brief Description of your Changes**
> >
>I fixed a couple of typos found in the /contributing/issues.md file. >I fixed a couple of typos found in the /contributing/issues.md file.
> >
>**Impact of your Changes** >**Impact of your Changes**
> >
>- Documentation is more clear and readable for the users. >- Documentation is more clear and readable for the users.
> >
>**Tests Performed** >**Tests Performed**
> >
>Ran `vitepress dev docs` and verified it was built successfully. >Ran `vitepress dev docs` and verified it was built successfully.
> >
>**Additional Comments** >**Additional Comments**
> >
>[At Discretion] >[At Discretion]
## Change in Behavior ## Change in Behavior
An example of an appropriate PR that adds a new feature would be: An example of an appropriate PR that adds a new feature would be:
> >
>**Brief Description of your Changes** >**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) >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** >**Impact of your Changes**
> >
>- jecs functionality is extended to better fit the needs of the community [explain why]. >- jecs functionality is extended to better fit the needs of the community [explain why].
> >
>**Tests Performed** >**Tests Performed**
> >
>Added a few test cases to ensure the function runs as expected [link to changes]. >Added a few test cases to ensure the function runs as expected [link to changes].
> >
>**Additional Comments** >**Additional Comments**
> >
>[At Discretion] >[At Discretion]
## Addons ## Addons
If you made something you think should be included into the [resources page](../../resources), let us know! 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. 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: One example of a PR that would be accepted is:
>**Brief Description of your Changes** >**Brief Description of your Changes**
> >
>I added `jecs observers` to the addons page. >I added `jecs observers` to the addons page.
> >
>**Impact of your Changes** >**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] >- 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] >- [talk about why you went with this design instead of maybe an alternative]
> >
>**Tests Performed** >**Tests Performed**
> >
> I used this tool in conjunction with jecs and ensured it works as expected. > 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.] > [If you wrote unit tests for your tool, mention it here.]
> >
>**Additional Comments** >**Additional Comments**
> >
>[At Discretion] >[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! 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 --!optimize 2
--!native --!native
--!strict --!strict
@ -117,6 +118,7 @@ local ECS_ID_MASK = 0b00
local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48
local NULL_ARRAY = table.freeze({}) :: Column local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false) local NULL = newproxy(false)
@ -168,7 +170,6 @@ end
local function ECS_COMBINE(id: number, generation: number): i53 local function ECS_COMBINE(id: number, generation: number): i53
return id + (generation * ECS_ENTITY_MASK) return id + (generation * ECS_ENTITY_MASK)
end end
local ECS_PAIR_OFFSET = 2^48
local function ECS_IS_PAIR(e: number): boolean local function ECS_IS_PAIR(e: number): boolean
return e > ECS_PAIR_OFFSET return e > ECS_PAIR_OFFSET
@ -2576,40 +2577,40 @@ export type World = {
component: <T>(self: World) -> Entity<T>, component: <T>(self: World) -> Entity<T>,
--- Gets the target of an relationship. For example, when a user calls --- Gets the target of an relationship. For example, when a user calls
--- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. --- `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. --- 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 --- 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 --- 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) -> (), cleanup: (self: World) -> (),
-- Clears an entity from the 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 --- 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. --- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(self: World, id: Entity, Id<A>) -> A?) get: & (<T, a>(World, Entity<T>, Id<a>) -> a?)
& (<A, B>(self: World, id: Entity, Id<A>, Id<B>) -> (A?, B?)) & (<T, a, b>(World, Entity<T>, Id<a>, Id<b>) -> (a?, b?))
& (<A, B, C>(self: World, id: Entity, Id<A>, Id<B>, Id<C>) -> (A?, B?, C?)) & (<T, a, b, c>(World, Entity<T>, 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?), & (<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. --- Returns whether the entity has the ID.
has: (<A>(World, Entity, A) -> boolean) has: (<T>(World, Entity<T>, Id) -> boolean)
& (<A, B>(World, Entity, A, B) -> boolean) & (<T>(World, Entity<T>, Id, Id) -> boolean)
& (<A, B, C>(World, Entity, A, B, C) -> boolean) & (<T>(World, Entity<T>, Id, Id, Id) -> boolean)
& <A, B, C, D>(World, Entity, A, B, C, D) -> 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. --- 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 --- 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 --- 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, each: <T>(self: World, id: Id<T>) -> () -> Entity,

View file

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

View file

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

View file

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

View file

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