mirror of
https://github.com/Ukendio/jecs.git
synced 2025-08-04 19:29:18 +00:00
Compare commits
3 commits
803616a005
...
8490dfd294
Author | SHA1 | Date | |
---|---|---|---|
|
8490dfd294 | ||
|
d505a0a38d | ||
|
e74924ec07 |
32 changed files with 2293 additions and 2290 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
@ -1,2 +1,2 @@
|
|||
*.luau text eol=lf
|
||||
*.html linguist-vendored
|
||||
*.luau text eol=lf
|
||||
*.html linguist-vendored
|
||||
|
|
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,15 +1,15 @@
|
|||
## Brief Description of your Changes.
|
||||
|
||||
Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
|
||||
|
||||
## Impact of your Changes
|
||||
|
||||
What implications will this have on the project? Will there be altered behavior or performance with this change?
|
||||
|
||||
## Tests Performed
|
||||
|
||||
What have you done to ensure this change has the least possible impact on the project?
|
||||
|
||||
## Additional Comments
|
||||
|
||||
## Brief Description of your Changes.
|
||||
|
||||
Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
|
||||
|
||||
## Impact of your Changes
|
||||
|
||||
What implications will this have on the project? Will there be altered behavior or performance with this change?
|
||||
|
||||
## Tests Performed
|
||||
|
||||
What have you done to ensure this change has the least possible impact on the project?
|
||||
|
||||
## Additional Comments
|
||||
|
||||
Anything else you feel is relevant.
|
146
.gitignore
vendored
146
.gitignore
vendored
|
@ -1,73 +1,73 @@
|
|||
# Compiled Lua sources
|
||||
luac.out
|
||||
|
||||
# luarocks build files
|
||||
*.src.rock
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rbxm
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.os
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
*.def
|
||||
*.exp
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Wally files
|
||||
DevPackages
|
||||
Packages
|
||||
wally.lock
|
||||
WallyPatches
|
||||
|
||||
# Typescript
|
||||
/node_modules
|
||||
/include
|
||||
|
||||
# Misc
|
||||
roblox.toml
|
||||
sourcemap.json
|
||||
drafts/
|
||||
|
||||
# Cached Vitepress (docs)
|
||||
|
||||
/docs/.vitepress/cache
|
||||
/docs/.vitepress/dist
|
||||
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
|
||||
# Luau tools
|
||||
profile.*
|
||||
|
||||
# Patch files
|
||||
|
||||
*.patch
|
||||
|
||||
genhtml.perl
|
||||
# Compiled Lua sources
|
||||
luac.out
|
||||
|
||||
# luarocks build files
|
||||
*.src.rock
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rbxm
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.os
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
*.def
|
||||
*.exp
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Wally files
|
||||
DevPackages
|
||||
Packages
|
||||
wally.lock
|
||||
WallyPatches
|
||||
|
||||
# Typescript
|
||||
/node_modules
|
||||
/include
|
||||
|
||||
# Misc
|
||||
roblox.toml
|
||||
sourcemap.json
|
||||
drafts/
|
||||
|
||||
# Cached Vitepress (docs)
|
||||
|
||||
/docs/.vitepress/cache
|
||||
/docs/.vitepress/dist
|
||||
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
|
||||
# Luau tools
|
||||
profile.*
|
||||
|
||||
# Patch files
|
||||
|
||||
*.patch
|
||||
|
||||
genhtml.perl
|
||||
|
|
20
.luaurc
20
.luaurc
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"aliases": {
|
||||
"jecs": "jecs",
|
||||
"testkit": "tools/testkit",
|
||||
"mirror": "mirror",
|
||||
"tools": "tools",
|
||||
"addons": "addons"
|
||||
},
|
||||
"languageMode": "strict"
|
||||
}
|
||||
{
|
||||
"aliases": {
|
||||
"jecs": "jecs",
|
||||
"testkit": "tools/testkit",
|
||||
"mirror": "mirror",
|
||||
"tools": "tools",
|
||||
"addons": "addons"
|
||||
},
|
||||
"languageMode": "strict"
|
||||
}
|
||||
|
|
18
.stylua.toml
18
.stylua.toml
|
@ -1,9 +1,9 @@
|
|||
syntax = "All"
|
||||
column_width = 120
|
||||
line_endings = "Unix"
|
||||
indent_type = "Tabs"
|
||||
indent_width = 4
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Always"
|
||||
space_after_function_names = "Never"
|
||||
collapse_simple_statement = "Never"
|
||||
syntax = "All"
|
||||
column_width = 120
|
||||
line_endings = "Unix"
|
||||
indent_type = "Tabs"
|
||||
indent_width = 4
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Always"
|
||||
space_after_function_names = "Never"
|
||||
collapse_simple_statement = "Never"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="white"/>
|
||||
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="white"/>
|
||||
</svg>
|
||||
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="white"/>
|
||||
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="white"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -1,6 +1,6 @@
|
|||
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="black"/>
|
||||
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="black"/>
|
||||
</svg>
|
||||
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="black"/>
|
||||
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="black"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }?
|
||||
}
|
||||
}>,
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +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, 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
|
||||
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
|
||||
|
|
|
@ -1,15 +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>
|
||||
export type Snapshot = {
|
||||
[string]: {
|
||||
set: { jecs.Entity }?,
|
||||
values: { any }?,
|
||||
removed: { jecs.Entity }?
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,190 +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")
|
||||
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
|
||||
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
|
||||
|
|
100
docs/api/jecs.md
100
docs/api/jecs.md
|
@ -1,50 +1,50 @@
|
|||
# Jecs
|
||||
|
||||
Jecs. Just an Entity Component System.
|
||||
|
||||
# Properties
|
||||
|
||||
## World
|
||||
```luau
|
||||
jecs.World: World
|
||||
```
|
||||
A world is a container of all ECS data. Games can have multiple worlds but component IDs may conflict between worlds. Ensure to register the same component IDs in the same order for each world.
|
||||
|
||||
## Wildcard
|
||||
```luau
|
||||
jecs.Wildcard: Entity
|
||||
```
|
||||
Builtin component type. This ID is used for wildcard queries.
|
||||
|
||||
## Component
|
||||
```luau
|
||||
jecs.Component: Entity
|
||||
```
|
||||
Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
|
||||
|
||||
## ChildOf
|
||||
```luau
|
||||
jecs.ChildOf: Entity
|
||||
```
|
||||
Builtin component type. This ID is for creating parent-child hierarchies.
|
||||
|
||||
## Rest
|
||||
```luau
|
||||
jecs.Rest: Entity
|
||||
```
|
||||
|
||||
# Functions
|
||||
|
||||
## pair()
|
||||
```luau
|
||||
function jecs.pair(
|
||||
first: Entity, -- The first element of the pair, referred to as the relationship of the relationship pair.
|
||||
object: Entity, -- The second element of the pair, referred to as the target of the relationship pair.
|
||||
): number -- Returns the ID with those two elements
|
||||
|
||||
```
|
||||
::: info
|
||||
|
||||
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
|
||||
|
||||
:::
|
||||
# Jecs
|
||||
|
||||
Jecs. Just an Entity Component System.
|
||||
|
||||
# Properties
|
||||
|
||||
## World
|
||||
```luau
|
||||
jecs.World: World
|
||||
```
|
||||
A world is a container of all ECS data. Games can have multiple worlds but component IDs may conflict between worlds. Ensure to register the same component IDs in the same order for each world.
|
||||
|
||||
## Wildcard
|
||||
```luau
|
||||
jecs.Wildcard: Entity
|
||||
```
|
||||
Builtin component type. This ID is used for wildcard queries.
|
||||
|
||||
## Component
|
||||
```luau
|
||||
jecs.Component: Entity
|
||||
```
|
||||
Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
|
||||
|
||||
## ChildOf
|
||||
```luau
|
||||
jecs.ChildOf: Entity
|
||||
```
|
||||
Builtin component type. This ID is for creating parent-child hierarchies.
|
||||
|
||||
## Rest
|
||||
```luau
|
||||
jecs.Rest: Entity
|
||||
```
|
||||
|
||||
# Functions
|
||||
|
||||
## pair()
|
||||
```luau
|
||||
function jecs.pair(
|
||||
first: Entity, -- The first element of the pair, referred to as the relationship of the relationship pair.
|
||||
object: Entity, -- The second element of the pair, referred to as the target of the relationship pair.
|
||||
): number -- Returns the ID with those two elements
|
||||
|
||||
```
|
||||
::: info
|
||||
|
||||
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
|
||||
|
||||
:::
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -1934,6 +1934,9 @@ local function query_cached(query: ecs_query_data_t)
|
|||
|
||||
local function on_delete_callback(archetype)
|
||||
local i = table.find(archetypes, archetype) :: number
|
||||
if i == nil then
|
||||
return
|
||||
end
|
||||
local n = #archetypes
|
||||
archetypes[i] = archetypes[n]
|
||||
archetypes[n] = nil
|
||||
|
|
316
test/lol.luau
316
test/lol.luau
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,177 +1,177 @@
|
|||
#!/usr/bin/python3
|
||||
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
||||
|
||||
# Given a profile dump, this tool generates a flame graph based on the stacks listed in the profile
|
||||
# The result of analysis is a .svg file which can be viewed in a browser
|
||||
|
||||
import svg
|
||||
import argparse
|
||||
import json
|
||||
|
||||
argumentParser = argparse.ArgumentParser(description='Generate flamegraph SVG from Luau sampling profiler dumps')
|
||||
argumentParser.add_argument('source_file', type=open)
|
||||
argumentParser.add_argument('--json', dest='useJson',action='store_const',const=1,default=0,help='Parse source_file as JSON')
|
||||
|
||||
class Node(svg.Node):
|
||||
def __init__(self):
|
||||
svg.Node.__init__(self)
|
||||
self.function = ""
|
||||
self.source = ""
|
||||
self.line = 0
|
||||
self.ticks = 0
|
||||
|
||||
def text(self):
|
||||
return self.function
|
||||
|
||||
def title(self):
|
||||
if self.line > 0:
|
||||
return "{}\n{}:{}".format(self.function, self.source, self.line)
|
||||
else:
|
||||
return self.function
|
||||
|
||||
def details(self, root):
|
||||
return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks)
|
||||
|
||||
|
||||
def nodeFromCallstackListFile(source_file):
|
||||
dump = source_file.readlines()
|
||||
root = Node()
|
||||
|
||||
for l in dump:
|
||||
ticks, stack = l.strip().split(" ", 1)
|
||||
node = root
|
||||
|
||||
for f in reversed(stack.split(";")):
|
||||
source, function, line = f.split(",")
|
||||
|
||||
child = node.child(f)
|
||||
child.function = function
|
||||
child.source = source
|
||||
child.line = int(line) if len(line) > 0 else 0
|
||||
|
||||
node = child
|
||||
|
||||
node.ticks += int(ticks)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def getDuration(nodes, nid):
|
||||
node = nodes[nid - 1]
|
||||
total = node['TotalDuration']
|
||||
|
||||
if 'NodeIds' in node:
|
||||
for cid in node['NodeIds']:
|
||||
total -= nodes[cid - 1]['TotalDuration']
|
||||
|
||||
return total
|
||||
|
||||
def getFunctionKey(fn):
|
||||
source = fn['Source'] if 'Source' in fn else ''
|
||||
name = fn['Name'] if 'Name' in fn else ''
|
||||
line = str(fn['Line']) if 'Line' in fn else '-1'
|
||||
|
||||
return source + "," + name + "," + line
|
||||
|
||||
def recursivelyBuildNodeTree(nodes, functions, parent, fid, nid):
|
||||
ninfo = nodes[nid - 1]
|
||||
finfo = functions[fid - 1]
|
||||
|
||||
child = parent.child(getFunctionKey(finfo))
|
||||
child.source = finfo['Source'] if 'Source' in finfo else ''
|
||||
child.function = finfo['Name'] if 'Name' in finfo else ''
|
||||
child.line = int(finfo['Line']) if 'Line' in finfo and finfo['Line'] > 0 else 0
|
||||
|
||||
child.ticks = getDuration(nodes, nid)
|
||||
|
||||
if 'FunctionIds' in ninfo:
|
||||
assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds']))
|
||||
|
||||
for i in range(0, len(ninfo['FunctionIds'])):
|
||||
recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i])
|
||||
|
||||
return
|
||||
|
||||
def nodeFromJSONV2(dump):
|
||||
assert(dump['Version'] == 2)
|
||||
|
||||
nodes = dump['Nodes']
|
||||
functions = dump['Functions']
|
||||
categories = dump['Categories']
|
||||
|
||||
root = Node()
|
||||
|
||||
for category in categories:
|
||||
nid = category['NodeId']
|
||||
node = nodes[nid - 1]
|
||||
name = category['Name']
|
||||
|
||||
child = root.child(name)
|
||||
child.function = name
|
||||
child.ticks = getDuration(nodes, nid)
|
||||
|
||||
if 'FunctionIds' in node:
|
||||
assert(len(node['FunctionIds']) == len(node['NodeIds']))
|
||||
|
||||
for i in range(0, len(node['FunctionIds'])):
|
||||
recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i])
|
||||
|
||||
return root
|
||||
|
||||
def getDurationV1(obj):
|
||||
total = obj['TotalDuration']
|
||||
|
||||
if 'Children' in obj:
|
||||
for key, obj in obj['Children'].items():
|
||||
total -= obj['TotalDuration']
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def nodeFromJSONObject(node, key, obj):
|
||||
source, function, line = key.split(",")
|
||||
|
||||
node.function = function
|
||||
node.source = source
|
||||
node.line = int(line) if len(line) > 0 else 0
|
||||
|
||||
node.ticks = getDurationV1(obj)
|
||||
|
||||
if 'Children' in obj:
|
||||
for key, obj in obj['Children'].items():
|
||||
nodeFromJSONObject(node.child(key), key, obj)
|
||||
|
||||
return node
|
||||
|
||||
def nodeFromJSONV1(dump):
|
||||
assert(dump['Version'] == 1)
|
||||
root = Node()
|
||||
|
||||
if 'Children' in dump:
|
||||
for key, obj in dump['Children'].items():
|
||||
nodeFromJSONObject(root.child(key), key, obj)
|
||||
|
||||
return root
|
||||
|
||||
def nodeFromJSONFile(source_file):
|
||||
dump = json.load(source_file)
|
||||
|
||||
if dump['Version'] == 2:
|
||||
return nodeFromJSONV2(dump)
|
||||
elif dump['Version'] == 1:
|
||||
return nodeFromJSONV1(dump)
|
||||
|
||||
return Node()
|
||||
|
||||
|
||||
arguments = argumentParser.parse_args()
|
||||
|
||||
if arguments.useJson:
|
||||
root = nodeFromJSONFile(arguments.source_file)
|
||||
else:
|
||||
root = nodeFromCallstackListFile(arguments.source_file)
|
||||
|
||||
|
||||
|
||||
svg.layout(root, lambda n: n.ticks)
|
||||
svg.display(root, "Flame Graph", "hot", flip = True)
|
||||
#!/usr/bin/python3
|
||||
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
||||
|
||||
# Given a profile dump, this tool generates a flame graph based on the stacks listed in the profile
|
||||
# The result of analysis is a .svg file which can be viewed in a browser
|
||||
|
||||
import svg
|
||||
import argparse
|
||||
import json
|
||||
|
||||
argumentParser = argparse.ArgumentParser(description='Generate flamegraph SVG from Luau sampling profiler dumps')
|
||||
argumentParser.add_argument('source_file', type=open)
|
||||
argumentParser.add_argument('--json', dest='useJson',action='store_const',const=1,default=0,help='Parse source_file as JSON')
|
||||
|
||||
class Node(svg.Node):
|
||||
def __init__(self):
|
||||
svg.Node.__init__(self)
|
||||
self.function = ""
|
||||
self.source = ""
|
||||
self.line = 0
|
||||
self.ticks = 0
|
||||
|
||||
def text(self):
|
||||
return self.function
|
||||
|
||||
def title(self):
|
||||
if self.line > 0:
|
||||
return "{}\n{}:{}".format(self.function, self.source, self.line)
|
||||
else:
|
||||
return self.function
|
||||
|
||||
def details(self, root):
|
||||
return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks)
|
||||
|
||||
|
||||
def nodeFromCallstackListFile(source_file):
|
||||
dump = source_file.readlines()
|
||||
root = Node()
|
||||
|
||||
for l in dump:
|
||||
ticks, stack = l.strip().split(" ", 1)
|
||||
node = root
|
||||
|
||||
for f in reversed(stack.split(";")):
|
||||
source, function, line = f.split(",")
|
||||
|
||||
child = node.child(f)
|
||||
child.function = function
|
||||
child.source = source
|
||||
child.line = int(line) if len(line) > 0 else 0
|
||||
|
||||
node = child
|
||||
|
||||
node.ticks += int(ticks)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def getDuration(nodes, nid):
|
||||
node = nodes[nid - 1]
|
||||
total = node['TotalDuration']
|
||||
|
||||
if 'NodeIds' in node:
|
||||
for cid in node['NodeIds']:
|
||||
total -= nodes[cid - 1]['TotalDuration']
|
||||
|
||||
return total
|
||||
|
||||
def getFunctionKey(fn):
|
||||
source = fn['Source'] if 'Source' in fn else ''
|
||||
name = fn['Name'] if 'Name' in fn else ''
|
||||
line = str(fn['Line']) if 'Line' in fn else '-1'
|
||||
|
||||
return source + "," + name + "," + line
|
||||
|
||||
def recursivelyBuildNodeTree(nodes, functions, parent, fid, nid):
|
||||
ninfo = nodes[nid - 1]
|
||||
finfo = functions[fid - 1]
|
||||
|
||||
child = parent.child(getFunctionKey(finfo))
|
||||
child.source = finfo['Source'] if 'Source' in finfo else ''
|
||||
child.function = finfo['Name'] if 'Name' in finfo else ''
|
||||
child.line = int(finfo['Line']) if 'Line' in finfo and finfo['Line'] > 0 else 0
|
||||
|
||||
child.ticks = getDuration(nodes, nid)
|
||||
|
||||
if 'FunctionIds' in ninfo:
|
||||
assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds']))
|
||||
|
||||
for i in range(0, len(ninfo['FunctionIds'])):
|
||||
recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i])
|
||||
|
||||
return
|
||||
|
||||
def nodeFromJSONV2(dump):
|
||||
assert(dump['Version'] == 2)
|
||||
|
||||
nodes = dump['Nodes']
|
||||
functions = dump['Functions']
|
||||
categories = dump['Categories']
|
||||
|
||||
root = Node()
|
||||
|
||||
for category in categories:
|
||||
nid = category['NodeId']
|
||||
node = nodes[nid - 1]
|
||||
name = category['Name']
|
||||
|
||||
child = root.child(name)
|
||||
child.function = name
|
||||
child.ticks = getDuration(nodes, nid)
|
||||
|
||||
if 'FunctionIds' in node:
|
||||
assert(len(node['FunctionIds']) == len(node['NodeIds']))
|
||||
|
||||
for i in range(0, len(node['FunctionIds'])):
|
||||
recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i])
|
||||
|
||||
return root
|
||||
|
||||
def getDurationV1(obj):
|
||||
total = obj['TotalDuration']
|
||||
|
||||
if 'Children' in obj:
|
||||
for key, obj in obj['Children'].items():
|
||||
total -= obj['TotalDuration']
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def nodeFromJSONObject(node, key, obj):
|
||||
source, function, line = key.split(",")
|
||||
|
||||
node.function = function
|
||||
node.source = source
|
||||
node.line = int(line) if len(line) > 0 else 0
|
||||
|
||||
node.ticks = getDurationV1(obj)
|
||||
|
||||
if 'Children' in obj:
|
||||
for key, obj in obj['Children'].items():
|
||||
nodeFromJSONObject(node.child(key), key, obj)
|
||||
|
||||
return node
|
||||
|
||||
def nodeFromJSONV1(dump):
|
||||
assert(dump['Version'] == 1)
|
||||
root = Node()
|
||||
|
||||
if 'Children' in dump:
|
||||
for key, obj in dump['Children'].items():
|
||||
nodeFromJSONObject(root.child(key), key, obj)
|
||||
|
||||
return root
|
||||
|
||||
def nodeFromJSONFile(source_file):
|
||||
dump = json.load(source_file)
|
||||
|
||||
if dump['Version'] == 2:
|
||||
return nodeFromJSONV2(dump)
|
||||
elif dump['Version'] == 1:
|
||||
return nodeFromJSONV1(dump)
|
||||
|
||||
return Node()
|
||||
|
||||
|
||||
arguments = argumentParser.parse_args()
|
||||
|
||||
if arguments.useJson:
|
||||
root = nodeFromJSONFile(arguments.source_file)
|
||||
else:
|
||||
root = nodeFromCallstackListFile(arguments.source_file)
|
||||
|
||||
|
||||
|
||||
svg.layout(root, lambda n: n.ticks)
|
||||
svg.display(root, "Flame Graph", "hot", flip = True)
|
||||
|
|
|
@ -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")
|
||||
|
|
1002
tools/svg.py
1002
tools/svg.py
File diff suppressed because it is too large
Load diff
|
@ -1,24 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"downlevelIteration": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "Roact.createElement",
|
||||
"jsxFragmentFactory": "Roact.Fragment",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node",
|
||||
"noLib": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"typeRoots": [
|
||||
"node_modules/@rbxts"
|
||||
],
|
||||
"rootDir": "lib",
|
||||
"outDir": "out",
|
||||
"baseUrl": "lib",
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
|
||||
"moduleDetection": "force"
|
||||
}
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"downlevelIteration": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "Roact.createElement",
|
||||
"jsxFragmentFactory": "Roact.Fragment",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node",
|
||||
"noLib": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"typeRoots": [
|
||||
"node_modules/@rbxts"
|
||||
],
|
||||
"rootDir": "lib",
|
||||
"outDir": "out",
|
||||
"baseUrl": "lib",
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
|
||||
"moduleDetection": "force"
|
||||
}
|
||||
}
|
||||
|
|
30
wally.toml
30
wally.toml
|
@ -1,15 +1,15 @@
|
|||
[package]
|
||||
name = "ukendio/jecs"
|
||||
version = "0.6.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
license = "MIT"
|
||||
include = [
|
||||
"default.project.json",
|
||||
"jecs.luau",
|
||||
"wally.toml",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
]
|
||||
exclude = ["**"]
|
||||
[package]
|
||||
name = "ukendio/jecs"
|
||||
version = "0.6.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
license = "MIT"
|
||||
include = [
|
||||
"default.project.json",
|
||||
"jecs.luau",
|
||||
"wally.toml",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
]
|
||||
exclude = ["**"]
|
||||
|
|
Loading…
Reference in a new issue