Initial push
This commit is contained in:
commit
ee69c03334
31 changed files with 1578 additions and 0 deletions
17
.darklua.json
Normal file
17
.darklua.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"process": [
|
||||||
|
{
|
||||||
|
"rule": "convert_require",
|
||||||
|
"current": {
|
||||||
|
"name": "path",
|
||||||
|
"sources": {
|
||||||
|
"@pkg": "Packages/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"name": "roblox",
|
||||||
|
"rojo_sourcemap": "sourcemap.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
71
.github/workflows/ci.yml
vendored
Normal file
71
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
name: Continous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Analyze
|
||||||
|
run: lune run analyze
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: |
|
||||||
|
selene crates/
|
||||||
|
|
||||||
|
style:
|
||||||
|
name: Styling
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check code style
|
||||||
|
uses: JohnnyMorganz/stylua-action@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: v0.20.0
|
||||||
|
args: --check crates/
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Unit Testing
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
lune run test/runner.luau
|
69
.github/workflows/release.yml
vendored
Normal file
69
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
lune run install-packages
|
||||||
|
|
||||||
|
- name: Upload Build Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: build.rbxm
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
needs: [build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Build
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: build
|
||||||
|
|
||||||
|
- name: Rename Build
|
||||||
|
run: mv build/build.rbxm jecs_utils.rbxm
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
name: Jecs Utils ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
jecs_utils.rbxm
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish
|
||||||
|
needs: [release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
|
- name: Wally Login
|
||||||
|
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: wally publish
|
57
.gitignore
vendored
Normal file
57
.gitignore
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Compiled Lua sources
|
||||||
|
luac.out
|
||||||
|
|
||||||
|
# luarocks build files
|
||||||
|
*.src.rock
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rbxm
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
globalTypes.d.luau
|
7
.luaurc
Normal file
7
.luaurc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"languageMode": "strict",
|
||||||
|
"aliases": {
|
||||||
|
"jecs_utils": "lib",
|
||||||
|
"testkit": "test/testkit"
|
||||||
|
}
|
||||||
|
}
|
17
.lune/analyze.luau
Normal file
17
.lune/analyze.luau
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
--!nocheck
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local function start_process(cmd: string)
|
||||||
|
local arguments = string.split(cmd, " ")
|
||||||
|
local command = arguments[1]
|
||||||
|
table.remove(arguments, 1)
|
||||||
|
|
||||||
|
process.spawn(command, arguments, { stdio = "forward" })
|
||||||
|
end
|
||||||
|
|
||||||
|
start_process("lune run install-packages.luau")
|
||||||
|
--start_process("curl -O https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.luau")
|
||||||
|
start_process("rojo sourcemap dev.project.json -o sourcemap.json")
|
||||||
|
start_process(
|
||||||
|
"luau-lsp analyze --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore Packages/**/*.lua --ignore Packages/**/*.luau lib/"
|
||||||
|
)
|
14
.lune/build.luau
Normal file
14
.lune/build.luau
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
--!nocheck
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local function start_process(cmd: string, env: { string }?)
|
||||||
|
local arguments = string.split(cmd, " ")
|
||||||
|
local command = arguments[1]
|
||||||
|
table.remove(arguments, 1)
|
||||||
|
|
||||||
|
process.spawn(command, arguments, { stdio = "forward", env = env })
|
||||||
|
end
|
||||||
|
|
||||||
|
start_process("lune run install-packages")
|
||||||
|
start_process("darklua process --config .darklua.json lib/ dist/", { "ROBLOX_DEV=false" })
|
||||||
|
start_process("rojo build build.project.json -o build.rbxm")
|
14
.lune/check.luau
Normal file
14
.lune/check.luau
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
--!nocheck
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local function start_process(cmd: string)
|
||||||
|
local arguments = string.split(cmd, " ")
|
||||||
|
local command = arguments[1]
|
||||||
|
table.remove(arguments, 1)
|
||||||
|
|
||||||
|
process.spawn(command, arguments, { stdio = "forward" })
|
||||||
|
end
|
||||||
|
|
||||||
|
start_process("lune run analyze")
|
||||||
|
start_process("stylua lib/")
|
||||||
|
start_process("selene lib/")
|
15
.lune/dev.luau
Normal file
15
.lune/dev.luau
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
--!nocheck
|
||||||
|
local process = require("@lune/process")
|
||||||
|
local task = require("@lune/task")
|
||||||
|
|
||||||
|
local function start_process(cmd: string, env: { string }?)
|
||||||
|
local arguments = string.split(cmd, " ")
|
||||||
|
local command = arguments[1]
|
||||||
|
table.remove(arguments, 1)
|
||||||
|
|
||||||
|
process.spawn(command, arguments, { stdio = "forward", env = env })
|
||||||
|
end
|
||||||
|
|
||||||
|
task.spawn(start_process, "rojo sourcemap dev.project.json -o sourcemap.json --watch")
|
||||||
|
task.spawn(start_process, "darklua process --config .darklua.json --watch lib/ dist/", { "ROBLOX_DEV=true" })
|
||||||
|
--task.spawn(start_process, "rojo serve dev.project.json")
|
14
.lune/install-packages.luau
Normal file
14
.lune/install-packages.luau
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
--!nocheck
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local function start_process(cmd: string, cwd: string?)
|
||||||
|
local arguments = string.split(cmd, " ")
|
||||||
|
local command = arguments[1]
|
||||||
|
table.remove(arguments, 1)
|
||||||
|
|
||||||
|
process.spawn(command, arguments, { stdio = "forward", cwd = cwd })
|
||||||
|
end
|
||||||
|
|
||||||
|
start_process("wally install")
|
||||||
|
start_process("rojo sourcemap dev.project.json -o sourcemap.json")
|
||||||
|
start_process("wally-package-types --sourcemap sourcemap.json Packages/")
|
51
.zed/settings.json
Normal file
51
.zed/settings.json
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Folder-specific settings
|
||||||
|
//
|
||||||
|
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||||
|
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"luau-lsp": {
|
||||||
|
"settings": {
|
||||||
|
"luau-lsp": {
|
||||||
|
"completion": {
|
||||||
|
"imports": {
|
||||||
|
"enabled": true,
|
||||||
|
"suggestServices": true,
|
||||||
|
"suggestRequires": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourcemap": {
|
||||||
|
"rojoProjectFile": "dev.project.json"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"mode": "relativeToFile",
|
||||||
|
"fileAliases": {
|
||||||
|
"@jecs_utils": "lib",
|
||||||
|
"@testkit": "test/testkit"
|
||||||
|
},
|
||||||
|
"directoryAliases": {
|
||||||
|
"@pkg": "Packages/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ext": {
|
||||||
|
"roblox": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"fflags": {
|
||||||
|
"override": {
|
||||||
|
"LuauTinyControlFlowAnalysis": "true"
|
||||||
|
},
|
||||||
|
"sync": true,
|
||||||
|
"enable_by_default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"TypeScript": {
|
||||||
|
"tab_size": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Mark-Marks
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# sapphire-utils
|
||||||
|
[](https://github.com/mark-marks/jecs-utils/actions/workflows/ci.yml)
|
||||||
|
[](https://wally.run/package/mark-marks/jecs-utils)
|
||||||
|
[](https://github.com/Mark-Marks/jecs-utils/blob/main/LICENSE)
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [collect](/lib/collect.luau) - Collects all arguments fired through the given signal, and drains the collection on iteration.
|
||||||
|
- [handle](/lib/handle.luau) - Wrap `jecs.World` functions for faster (DX wise) operating on entities
|
||||||
|
- [replicator](/lib/replicator.luau) - Keep track of all entities with the passed components and calculate differences
|
||||||
|
- [ref](/lib/ref.luau) - Reference entities by key
|
||||||
|
- [command_buffer](/lib/command_buffer.luau) - Buffer commands to prevent query invalidation
|
6
build.project.json
Normal file
6
build.project.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "jecs-utils",
|
||||||
|
"tree": {
|
||||||
|
"$path": "dist"
|
||||||
|
}
|
||||||
|
}
|
6
default.project.json
Normal file
6
default.project.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "jecs-utils",
|
||||||
|
"tree": {
|
||||||
|
"$path": "lib"
|
||||||
|
}
|
||||||
|
}
|
15
dev.project.json
Normal file
15
dev.project.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"Packages": {
|
||||||
|
"$path": "Packages",
|
||||||
|
"jecs_utils": {
|
||||||
|
"$path": "lib"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
lib/collect.luau
Normal file
73
lib/collect.luau
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
|
||||||
|
--[[
|
||||||
|
original author by @memorycode
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Michael
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
--]]
|
||||||
|
|
||||||
|
--- What signals passed to `collect()` should be able to be coerced into
|
||||||
|
export type signal_like<D, T...> = { connect: confn<D, T...>, [any]: any } | { Connect: confn<D, T...>, [any]: any }
|
||||||
|
type confn<D, T...> = (self: signal_like<D, T...>, (T...) -> ()) -> D
|
||||||
|
|
||||||
|
--- Collects all arguments fired through the given signal, and drains the collection on iteration.\
|
||||||
|
--- Expects signals to have a `Connect` ***method***.
|
||||||
|
--- ```luau
|
||||||
|
--- local sig = collect(some_signal)
|
||||||
|
---
|
||||||
|
--- -- Imagine this as an ECS scheduler loop
|
||||||
|
--- while task.wait() do
|
||||||
|
--- for index, arg1 in sig do -- arg1, arg2, etc
|
||||||
|
--- print(arg1)
|
||||||
|
--- end
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
--- @param event signal<T...>
|
||||||
|
--- @return () -> (number, T...), D -- iterator and disconnector
|
||||||
|
local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T...), D)
|
||||||
|
local storage = {}
|
||||||
|
local mt = {}
|
||||||
|
local iter = function()
|
||||||
|
local n = #storage
|
||||||
|
return function(): (number?, T...)
|
||||||
|
if n <= 0 then
|
||||||
|
mt.__iter = nil
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
n -= 1
|
||||||
|
return n + 1, unpack(table.remove(storage, 1) :: any)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local disconnect = event:Connect(function(...)
|
||||||
|
table.insert(storage, { ... })
|
||||||
|
mt.__iter = iter :: any
|
||||||
|
end)
|
||||||
|
|
||||||
|
setmetatable(storage, mt)
|
||||||
|
return storage :: any, disconnect
|
||||||
|
end
|
||||||
|
|
||||||
|
return collect
|
114
lib/command_buffer.luau
Normal file
114
lib/command_buffer.luau
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local jecs = require("@pkg/jecs")
|
||||||
|
type entity<T = nil> = jecs.Entity<T>
|
||||||
|
type id<T = nil> = jecs.Id<T>
|
||||||
|
|
||||||
|
local world = require("./world").get()
|
||||||
|
|
||||||
|
--- `map<component_id, array<entity_id>>`
|
||||||
|
local add_commands: { [id]: { entity } } = {}
|
||||||
|
--- `map<component_id, array<entity_id, component_value>>`
|
||||||
|
local set_commands: { [id]: { [entity]: any } } = {}
|
||||||
|
--- `map<component_id, array<entity_id>>`
|
||||||
|
local remove_commands: { [id]: { entity } } = {}
|
||||||
|
--- `array<entity_id>`
|
||||||
|
local delete_commands: { entity } = {}
|
||||||
|
|
||||||
|
type command_buffer = {
|
||||||
|
--- Execute all buffered commands and clear the buffer
|
||||||
|
flush: () -> (),
|
||||||
|
|
||||||
|
--- Adds a component to the entity with no value
|
||||||
|
add: (entity: entity, component: id) -> (),
|
||||||
|
--- Assigns a value to a component on the given entity
|
||||||
|
set: <T>(entity: entity, component: id<T>, data: T) -> (),
|
||||||
|
--- Removes a component from the given entity
|
||||||
|
remove: (entity: entity, component: id) -> (),
|
||||||
|
--- Deletes an entity from the world
|
||||||
|
delete: (entity: entity) -> (),
|
||||||
|
}
|
||||||
|
|
||||||
|
local function flush()
|
||||||
|
local adds = add_commands
|
||||||
|
local sets = set_commands
|
||||||
|
local removes = remove_commands
|
||||||
|
local deletes = delete_commands
|
||||||
|
table.clear(add_commands)
|
||||||
|
table.clear(set_commands)
|
||||||
|
table.clear(remove_commands)
|
||||||
|
table.clear(delete_commands)
|
||||||
|
|
||||||
|
for _, id in deletes do
|
||||||
|
world:delete(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
for component, ids in adds do
|
||||||
|
for _, id in ids do
|
||||||
|
if deletes[id] then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
world:add(id, component)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for component, ids in sets do
|
||||||
|
for id, value in ids do
|
||||||
|
if deletes[id] then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
world:set(id, component, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for component, ids in removes do
|
||||||
|
for _, id in ids do
|
||||||
|
if deletes[id] then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
world:remove(id, component)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add(entity: entity, component: id)
|
||||||
|
if not add_commands[component] then
|
||||||
|
add_commands[component] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(add_commands[component], entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set<T>(entity: entity, component: id<T>, data: T)
|
||||||
|
if not set_commands[component] then
|
||||||
|
set_commands[component] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
set_commands[component][entity] = data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove(entity: entity, component: id)
|
||||||
|
if not remove_commands[component] then
|
||||||
|
remove_commands[component] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(remove_commands[component], entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function delete(entity: entity)
|
||||||
|
table.insert(delete_commands, entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
local command_buffer: command_buffer = {
|
||||||
|
flush = flush,
|
||||||
|
|
||||||
|
add = add,
|
||||||
|
set = set,
|
||||||
|
remove = remove,
|
||||||
|
delete = delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
return command_buffer
|
77
lib/handle.luau
Normal file
77
lib/handle.luau
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local jecs = require("@pkg/jecs")
|
||||||
|
export type entity<T = nil> = jecs.Entity<T>
|
||||||
|
export type id<T = nil> = entity<T> | jecs.Pair
|
||||||
|
|
||||||
|
local world = require("./world").get()
|
||||||
|
|
||||||
|
type interface = {
|
||||||
|
__index: interface,
|
||||||
|
|
||||||
|
new: (entity: entity) -> handle,
|
||||||
|
|
||||||
|
--- Checks if the entity has all of the given components
|
||||||
|
has: (self: handle, ...id) -> boolean,
|
||||||
|
--- Retrieves the value of up to 4 components. These values may be nil.
|
||||||
|
get: (<A>(self: handle, id<A>) -> A?)
|
||||||
|
& (<A, B>(self: handle, id<A>, id<B>) -> (A?, B?))
|
||||||
|
& (<A, B, C>(self: handle, id<A>, id<B>, id<C>) -> (A?, B?, C?))
|
||||||
|
& (<A, B, C, D>(self: handle, id<A>, id<B>, id<C>, id<D>) -> (A?, B?, C?, D?)),
|
||||||
|
--- Adds a component to the entity with no value
|
||||||
|
add: <T>(self: handle, id: id<T>) -> handle,
|
||||||
|
--- Assigns a value to a component on the given entity
|
||||||
|
set: <T>(self: handle, id: id<T>, data: T) -> handle,
|
||||||
|
--- Removes a component from the given entity
|
||||||
|
remove: (self: handle, id: id) -> handle,
|
||||||
|
--- Deletes the entity and all its related components and relationships. **Does not** refer to deleting the handle
|
||||||
|
delete: (self: handle) -> (),
|
||||||
|
--- Gets the entitys id
|
||||||
|
id: (self: handle) -> entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type handle = typeof(setmetatable({} :: { entity: entity }, {} :: interface))
|
||||||
|
|
||||||
|
local handle = {} :: interface
|
||||||
|
handle.__index = handle
|
||||||
|
|
||||||
|
function handle.new(entity: entity)
|
||||||
|
local self = {
|
||||||
|
entity = entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
return setmetatable(self, handle)
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle:has(...: id): boolean
|
||||||
|
return world:has(self.entity, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.get = function(self: handle, a: id, b: id?, c: id?, d: id?)
|
||||||
|
return world:get(self.entity, a, b :: any, c :: any, d :: any)
|
||||||
|
end :: any
|
||||||
|
|
||||||
|
function handle:add<T>(id: id<T>): handle
|
||||||
|
world:add(self.entity, id)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle:set<T>(id: id<T>, value: T): handle
|
||||||
|
world:set(self.entity, id, value)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle:remove(id: id): handle
|
||||||
|
world:remove(self.entity, id)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle:delete()
|
||||||
|
world:delete(self.entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
function handle:id(): entity
|
||||||
|
return self.entity
|
||||||
|
end
|
||||||
|
|
||||||
|
return handle
|
26
lib/init.luau
Normal file
26
lib/init.luau
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local WORLD = require("./world")
|
||||||
|
local collect = require("./collect")
|
||||||
|
local command_buffer = require("./command_buffer")
|
||||||
|
local handle = require("./handle")
|
||||||
|
local jecs = require("@pkg/jecs")
|
||||||
|
local ref = require("./ref")
|
||||||
|
local replicator = require("./replicator")
|
||||||
|
|
||||||
|
--- Set the world for all utilities.
|
||||||
|
--- Should be called once per context before any utility is used.
|
||||||
|
--- @param world jecs.World
|
||||||
|
local function initialize(world: jecs.World)
|
||||||
|
WORLD.set(world)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize = initialize,
|
||||||
|
|
||||||
|
collect = collect,
|
||||||
|
handle = handle,
|
||||||
|
replicator = replicator,
|
||||||
|
ref = ref,
|
||||||
|
command_buffer = command_buffer,
|
||||||
|
}
|
27
lib/ref.luau
Normal file
27
lib/ref.luau
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local handle = require("./handle")
|
||||||
|
local world = require("./world").get()
|
||||||
|
|
||||||
|
local refs = {}
|
||||||
|
|
||||||
|
--- Gets an entity the given key references to.
|
||||||
|
--- If the key is nil, an entirely new entity is created and returned.
|
||||||
|
--- If the key doesn't reference an entity, a new entity is made for it to reference and returned.
|
||||||
|
--- @param key any
|
||||||
|
--- @return handle
|
||||||
|
local function ref(key: any): handle.handle
|
||||||
|
if not key then
|
||||||
|
return handle.new(world:entity())
|
||||||
|
end
|
||||||
|
|
||||||
|
local entity = refs[key]
|
||||||
|
if not entity then
|
||||||
|
entity = world:entity()
|
||||||
|
refs[key] = entity
|
||||||
|
end
|
||||||
|
|
||||||
|
return handle.new(entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ref
|
246
lib/replicator.luau
Normal file
246
lib/replicator.luau
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local jecs = require("@pkg/jecs")
|
||||||
|
type entity<T = nil> = jecs.Entity<T>
|
||||||
|
type i53 = number
|
||||||
|
|
||||||
|
local ref = require("./ref")
|
||||||
|
local world = require("./world").get()
|
||||||
|
|
||||||
|
--- A replicator keeps track of all entities with the passed components and their values -
|
||||||
|
--- whenever a component is changed (add, change, remove) and the replicator listens to it, it's also changed within the contained raw data.\
|
||||||
|
--- The developer can then calculate the difference on the server and send it to the client every time,
|
||||||
|
--- on which the difference is then applied to the world.\
|
||||||
|
--- Albeit it's called a replicator, it doesn't replicate the data by itself.
|
||||||
|
--- This allows the developer to use any networking libary to replicate the changes.
|
||||||
|
--- ```luau
|
||||||
|
--- -- server
|
||||||
|
--- local replicator = jecs_utils.create_replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- local function system()
|
||||||
|
--- local difference = replicator.calculate_difference()
|
||||||
|
--- -- There might not be any difference
|
||||||
|
--- if not difference then
|
||||||
|
--- return
|
||||||
|
--- end
|
||||||
|
--- data_replication_event.send_to_all(difference)
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
--- ```luau
|
||||||
|
--- -- client
|
||||||
|
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- local function system()
|
||||||
|
--- for _, difference in data_replication_event.poll() do
|
||||||
|
--- replicator.apply_difference(difference)
|
||||||
|
--- end
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
export type replicator = {
|
||||||
|
--- Gets the full data representing the entire world.
|
||||||
|
--- Useful for initial replication to every player.
|
||||||
|
--- ```luau
|
||||||
|
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- Players.PlayerAdded:Connect(function(player)
|
||||||
|
--- data_replication_event.send_to(player, replicator.get_full_data())
|
||||||
|
--- end)
|
||||||
|
--- ```
|
||||||
|
--- @return changes
|
||||||
|
get_full_data: () -> changes,
|
||||||
|
--- Calculates the difference between last sent data and currently stored data.
|
||||||
|
--- ```luau
|
||||||
|
--- local replicator = jecs_utils.create_replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- local function system()
|
||||||
|
--- local difference = replicator.calculate_difference()
|
||||||
|
--- -- There might not be any difference
|
||||||
|
--- if not difference then
|
||||||
|
--- return
|
||||||
|
--- end
|
||||||
|
--- data_replication_event.send_to_all(difference)
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
--- @return changes? -- There might not be any difference
|
||||||
|
calculate_difference: () -> changes?,
|
||||||
|
--- Applies the difference to the current data.
|
||||||
|
--- ```luau
|
||||||
|
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- local function system()
|
||||||
|
--- for _, difference in data_replication_event.poll() do
|
||||||
|
--- replicator.apply_difference(difference)
|
||||||
|
--- end
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
--- @param difference changes
|
||||||
|
apply_difference: (difference: changes) -> (),
|
||||||
|
}
|
||||||
|
|
||||||
|
--- `map<component_id, array<entity_id>>`
|
||||||
|
type changes_added = { [i53]: { i53 } }
|
||||||
|
--- `map<component_id, array<entity_id, component_value>>`
|
||||||
|
type changes_set = { [i53]: { [i53]: unknown } }
|
||||||
|
--- `map<component_id, array<entity_id>>`
|
||||||
|
type changes_removed = { [i53]: { i53 } }
|
||||||
|
|
||||||
|
export type changes = {
|
||||||
|
added: changes_added,
|
||||||
|
set: changes_set,
|
||||||
|
removed: changes_removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- A replicator keeps track of all entities with the passed components and their values -
|
||||||
|
--- whenever a component is changed (add, change, remove) and the replicator listens to it, it's also changed within the contained raw data.\
|
||||||
|
--- The developer can then calculate the difference on the server and send it to the client every time,
|
||||||
|
--- on which the difference is then applied to the world.\
|
||||||
|
--- Albeit it's called a replicator, it doesn't replicate the data by itself.
|
||||||
|
--- This allows the developer to use any networking libary to replicate the changes.
|
||||||
|
--- ```luau
|
||||||
|
--- -- server
|
||||||
|
--- local replicator = jecs_utils.create_replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- local function system()
|
||||||
|
--- local difference = replicator.calculate_difference()
|
||||||
|
--- -- There might not be any difference
|
||||||
|
--- if not difference then
|
||||||
|
--- return
|
||||||
|
--- end
|
||||||
|
--- data_replication_event.send_to_all(difference)
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
--- ```luau
|
||||||
|
--- -- client
|
||||||
|
--- local replicator = jecs_utils.replicator(component_a, component_b, ...)
|
||||||
|
---
|
||||||
|
--- local function system()
|
||||||
|
--- for _, difference in data_replication_event.poll() do
|
||||||
|
--- replicator.apply_difference(difference)
|
||||||
|
--- end
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
|
--- @param ... entity
|
||||||
|
--- @return replicator
|
||||||
|
local function replicator(...: entity): replicator
|
||||||
|
local components = { ... }
|
||||||
|
|
||||||
|
-- don't index a changes table start
|
||||||
|
local raw_added: changes_added = {}
|
||||||
|
local raw_set: changes_set = {}
|
||||||
|
local raw_removed: changes_removed = {}
|
||||||
|
|
||||||
|
local changes_added: changes_added = {}
|
||||||
|
local changes_set: changes_set = {}
|
||||||
|
local changes_removed: changes_removed = {}
|
||||||
|
-- don't index a changes table end
|
||||||
|
|
||||||
|
for _, component in components do
|
||||||
|
world:set(component, jecs.OnAdd, function(entity)
|
||||||
|
if not raw_added[component] then
|
||||||
|
raw_added[component] = {}
|
||||||
|
end
|
||||||
|
if not changes_added[component] then
|
||||||
|
changes_added[component] = {}
|
||||||
|
end
|
||||||
|
table.insert(raw_added[component], entity)
|
||||||
|
table.insert(changes_added[component], entity)
|
||||||
|
end)
|
||||||
|
world:set(component, jecs.OnSet, function(entity, value)
|
||||||
|
if not raw_set[component] then
|
||||||
|
raw_set[component] = {}
|
||||||
|
end
|
||||||
|
if not changes_set[component] then
|
||||||
|
changes_set[component] = {}
|
||||||
|
end
|
||||||
|
raw_set[component][entity] = value
|
||||||
|
changes_set[component][entity] = value
|
||||||
|
end)
|
||||||
|
world:set(component, jecs.OnRemove, function(entity)
|
||||||
|
if not raw_removed[component] then
|
||||||
|
raw_removed[component] = {}
|
||||||
|
end
|
||||||
|
if not changes_removed[component] then
|
||||||
|
changes_removed[component] = {}
|
||||||
|
end
|
||||||
|
table.insert(raw_removed[component], entity)
|
||||||
|
table.insert(changes_removed[component], entity)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_full_data(): changes
|
||||||
|
return {
|
||||||
|
added = raw_added,
|
||||||
|
set = raw_set,
|
||||||
|
removed = raw_removed,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function calculate_difference(): changes?
|
||||||
|
local difference_added = changes_added
|
||||||
|
local difference_set = changes_set
|
||||||
|
local difference_removed = changes_removed
|
||||||
|
changes_added = {}
|
||||||
|
changes_set = {}
|
||||||
|
changes_removed = {}
|
||||||
|
|
||||||
|
local added_not_empty = next(difference_added) ~= nil
|
||||||
|
local set_not_empty = next(difference_set) ~= nil
|
||||||
|
local removed_not_empty = next(difference_removed) ~= nil
|
||||||
|
|
||||||
|
if not added_not_empty and not set_not_empty and not removed_not_empty then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
added = difference_added,
|
||||||
|
set = difference_set,
|
||||||
|
removed = difference_removed,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_difference(difference: changes)
|
||||||
|
for component, entities in difference.added do
|
||||||
|
for _, entity_id in entities do
|
||||||
|
local entity = ref(`replicated-{entity_id}`)
|
||||||
|
|
||||||
|
local exists = entity:has(component)
|
||||||
|
if exists then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
entity:add(component)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for component, entities in difference.set do
|
||||||
|
for entity_id, value in entities do
|
||||||
|
local entity = ref(`replicated-{entity_id}`)
|
||||||
|
|
||||||
|
local existing_value = entity:get(component)
|
||||||
|
if existing_value == value then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
entity:set(component, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for component, entities in difference.removed do
|
||||||
|
for _, entity_id in entities do
|
||||||
|
local entity = ref(`replicated-{entity_id}`)
|
||||||
|
|
||||||
|
local exists = entity:has(component)
|
||||||
|
if exists then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
entity:remove(component)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
get_full_data = get_full_data,
|
||||||
|
calculate_difference = calculate_difference,
|
||||||
|
apply_difference = apply_difference,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return replicator
|
18
lib/world.luau
Normal file
18
lib/world.luau
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
--!strict
|
||||||
|
--!optimize 2
|
||||||
|
local jecs = require("@pkg/jecs")
|
||||||
|
|
||||||
|
local WORLD: jecs.World
|
||||||
|
|
||||||
|
local function get(): jecs.World
|
||||||
|
return WORLD
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set(world: jecs.World)
|
||||||
|
WORLD = world
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
get = get,
|
||||||
|
set = set,
|
||||||
|
}
|
9
luau_lsp_settings.json
Normal file
9
luau_lsp_settings.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"luau-lsp.fflags.override": {
|
||||||
|
"LuauTinyControlFlowAnalysis": "true"
|
||||||
|
},
|
||||||
|
"luau-lsp.require.mode": "relativeToFile",
|
||||||
|
"luau-lsp.require.directoryAliases": {
|
||||||
|
"@pkg": "Packages/"
|
||||||
|
}
|
||||||
|
}
|
14
rokit.toml
Normal file
14
rokit.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
|
||||||
|
# For more information, see https://github.com/rojo-rbx/rokit
|
||||||
|
|
||||||
|
# New tools can be added by running `rokit add <tool>` in a terminal.
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
wally = "upliftgames/wally@0.3.2"
|
||||||
|
rojo = "rojo-rbx/rojo@7.4.4"
|
||||||
|
lune = "lune-org/lune@0.8.8"
|
||||||
|
selene = "kampfkarren/selene@0.27.1"
|
||||||
|
luau-lsp = "johnnymorganz/luau-lsp@1.32.4"
|
||||||
|
stylua = "johnnymorganz/stylua@0.20.0"
|
||||||
|
wally-package-types = "johnnymorganz/wally-package-types@1.3.2"
|
||||||
|
darklua = "seaofvoices/darklua@0.13.1"
|
1
selene.toml
Normal file
1
selene.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
std = "selene_definitions"
|
6
selene_definitions.yaml
Normal file
6
selene_definitions.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
base: roblox
|
||||||
|
name: selene_definitions
|
||||||
|
globals:
|
||||||
|
require:
|
||||||
|
args:
|
||||||
|
- type: string
|
10
stylua.toml
Normal file
10
stylua.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
column_width = 120
|
||||||
|
line_endings = "Unix"
|
||||||
|
indent_type = "Spaces"
|
||||||
|
indent_width = 4
|
||||||
|
quote_style = "AutoPreferDouble"
|
||||||
|
call_parentheses = "Always"
|
||||||
|
collapse_simple_statement = "Never"
|
||||||
|
|
||||||
|
[sort_requires]
|
||||||
|
enabled = true
|
0
test/runner.luau
Normal file
0
test/runner.luau
Normal file
529
test/testkit.luau
Normal file
529
test/testkit.luau
Normal file
|
@ -0,0 +1,529 @@
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- testkit.luau
|
||||||
|
-- v0.7.3
|
||||||
|
-- MIT License
|
||||||
|
-- Copyright (c) 2022 centau
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local disable_ansi = false
|
||||||
|
|
||||||
|
local color = {
|
||||||
|
white_underline = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[1;4m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
white = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[37;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
green = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[32;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
red = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[31;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
yellow = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[33;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
red_highlight = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[41;1;30m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
green_highlight = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[42;1;30m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
gray = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[38;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
orange = function(s: string): string
|
||||||
|
return if disable_ansi then s else `\27[38;5;208m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function convert_units(unit: string, value: number): (number, string)
|
||||||
|
local sign = math.sign(value)
|
||||||
|
value = math.abs(value)
|
||||||
|
|
||||||
|
local prefix_colors = {
|
||||||
|
[4] = color.red,
|
||||||
|
[3] = color.red,
|
||||||
|
[2] = color.yellow,
|
||||||
|
[1] = color.yellow,
|
||||||
|
[0] = color.green,
|
||||||
|
[-1] = color.red,
|
||||||
|
[-2] = color.yellow,
|
||||||
|
[-3] = color.green,
|
||||||
|
[-4] = color.red,
|
||||||
|
}
|
||||||
|
|
||||||
|
local prefixes = {
|
||||||
|
[4] = "T",
|
||||||
|
[3] = "G",
|
||||||
|
[2] = "M",
|
||||||
|
[1] = "k",
|
||||||
|
[0] = " ",
|
||||||
|
[-1] = "m",
|
||||||
|
[-2] = "u",
|
||||||
|
[-3] = "n",
|
||||||
|
[-4] = "p",
|
||||||
|
}
|
||||||
|
|
||||||
|
local order = 0
|
||||||
|
|
||||||
|
while value >= 1000 do
|
||||||
|
order += 1
|
||||||
|
value /= 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
while value ~= 0 and value < 1 do
|
||||||
|
order -= 1
|
||||||
|
value *= 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
if value >= 100 then
|
||||||
|
value = math.floor(value)
|
||||||
|
elseif value >= 10 then
|
||||||
|
value = math.floor(value * 1e1) / 1e1
|
||||||
|
elseif value >= 1 then
|
||||||
|
value = math.floor(value * 1e2) / 1e2
|
||||||
|
end
|
||||||
|
|
||||||
|
return value * sign, prefix_colors[order](prefixes[order] .. unit)
|
||||||
|
end
|
||||||
|
|
||||||
|
local WALL = color.gray("│")
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Testing
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Test = {
|
||||||
|
name: string,
|
||||||
|
case: Case?,
|
||||||
|
cases: { Case },
|
||||||
|
duration: number,
|
||||||
|
error: {
|
||||||
|
message: string,
|
||||||
|
trace: string,
|
||||||
|
}?,
|
||||||
|
focus: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Case = {
|
||||||
|
name: string,
|
||||||
|
result: number,
|
||||||
|
line: number?,
|
||||||
|
focus: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
local PASS, FAIL, NONE, ERROR, SKIPPED = 1, 2, 3, 4, 5
|
||||||
|
|
||||||
|
local check_for_focused = false
|
||||||
|
local skip = false
|
||||||
|
local test: Test?
|
||||||
|
local tests: { Test } = {}
|
||||||
|
|
||||||
|
local function output_test_result(test: Test)
|
||||||
|
if check_for_focused then
|
||||||
|
local any_focused = test.focus
|
||||||
|
for _, case in test.cases do
|
||||||
|
any_focused = any_focused or case.focus
|
||||||
|
end
|
||||||
|
|
||||||
|
if not any_focused then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print(color.white(test.name))
|
||||||
|
|
||||||
|
for _, case in test.cases do
|
||||||
|
local status = ({
|
||||||
|
[PASS] = color.green("PASS"),
|
||||||
|
[FAIL] = color.red("FAIL"),
|
||||||
|
[NONE] = color.orange("NONE"),
|
||||||
|
[ERROR] = color.red("FAIL"),
|
||||||
|
[SKIPPED] = color.yellow("SKIP"),
|
||||||
|
})[case.result]
|
||||||
|
|
||||||
|
local line = case.result == FAIL and color.red(`{case.line}:`) or ""
|
||||||
|
if check_for_focused and case.focus == false and test.focus == false then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
print(`{status}{WALL} {line}{color.gray(case.name)}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
if test.error then
|
||||||
|
print(color.gray("error: ") .. color.red(test.error.message))
|
||||||
|
print(color.gray("trace: ") .. color.red(test.error.trace))
|
||||||
|
else
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CASE(name: string)
|
||||||
|
skip = false
|
||||||
|
assert(test, "no active test")
|
||||||
|
|
||||||
|
local case = {
|
||||||
|
name = name,
|
||||||
|
result = NONE,
|
||||||
|
focus = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
test.case = case
|
||||||
|
table.insert(test.cases, case)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CHECK<T>(value: T, stack: number?): T?
|
||||||
|
assert(test, "no active test")
|
||||||
|
|
||||||
|
local case = test.case
|
||||||
|
|
||||||
|
if not case then
|
||||||
|
CASE("")
|
||||||
|
case = test.case
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(case, "no active case")
|
||||||
|
|
||||||
|
if case.result ~= FAIL then
|
||||||
|
case.result = value and PASS or FAIL
|
||||||
|
if skip then
|
||||||
|
case.result = SKIPPED
|
||||||
|
end
|
||||||
|
case.line = debug.info(stack and stack + 1 or 2, "l")
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TEST(name: string, fn: () -> ())
|
||||||
|
local active = test
|
||||||
|
assert(not active, "cannot start test while another test is in progress")
|
||||||
|
|
||||||
|
test = {
|
||||||
|
name = name,
|
||||||
|
cases = {},
|
||||||
|
duration = 0,
|
||||||
|
focus = false,
|
||||||
|
}
|
||||||
|
assert(test)
|
||||||
|
|
||||||
|
table.insert(tests, test)
|
||||||
|
|
||||||
|
local start = os.clock()
|
||||||
|
local err
|
||||||
|
local success = xpcall(fn, function(m: string)
|
||||||
|
err = { message = m, trace = debug.traceback(nil, 2) }
|
||||||
|
end)
|
||||||
|
test.duration = os.clock() - start
|
||||||
|
|
||||||
|
if not test.case then
|
||||||
|
CASE("")
|
||||||
|
end
|
||||||
|
assert(test.case, "no active case")
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
test.case.result = ERROR
|
||||||
|
test.error = err
|
||||||
|
end
|
||||||
|
|
||||||
|
test = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FOCUS()
|
||||||
|
assert(test, "no active test")
|
||||||
|
|
||||||
|
check_for_focused = true
|
||||||
|
if test.case then
|
||||||
|
test.case.focus = true
|
||||||
|
else
|
||||||
|
test.focus = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FINISH(): boolean
|
||||||
|
local success = true
|
||||||
|
local total_cases = 0
|
||||||
|
local passed_cases = 0
|
||||||
|
local passed_focus_cases = 0
|
||||||
|
local total_focus_cases = 0
|
||||||
|
local duration = 0
|
||||||
|
|
||||||
|
for _, test in tests do
|
||||||
|
duration += test.duration
|
||||||
|
for _, case in test.cases do
|
||||||
|
total_cases += 1
|
||||||
|
if case.focus or test.focus then
|
||||||
|
total_focus_cases += 1
|
||||||
|
end
|
||||||
|
if case.result == PASS or case.result == NONE or case.result == SKIPPED then
|
||||||
|
if case.focus or test.focus then
|
||||||
|
passed_focus_cases += 1
|
||||||
|
end
|
||||||
|
passed_cases += 1
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
output_test_result(test)
|
||||||
|
end
|
||||||
|
|
||||||
|
print(color.gray(string.format(`{passed_cases}/{total_cases} test cases passed in %.3f ms.`, duration * 1e3)))
|
||||||
|
if check_for_focused then
|
||||||
|
print(color.gray(`{passed_focus_cases}/{total_focus_cases} focused test cases passed`))
|
||||||
|
end
|
||||||
|
|
||||||
|
local fails = total_cases - passed_cases
|
||||||
|
|
||||||
|
print((fails > 0 and color.red or color.green)(`{fails} {fails == 1 and "fail" or "fails"}`))
|
||||||
|
|
||||||
|
check_for_focused = false
|
||||||
|
return success, table.clear(tests)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SKIP(name: string)
|
||||||
|
skip = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Benchmarking
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Bench = {
|
||||||
|
time_start: number?,
|
||||||
|
memory_start: number?,
|
||||||
|
iterations: number?,
|
||||||
|
}
|
||||||
|
|
||||||
|
local bench: Bench?
|
||||||
|
|
||||||
|
function START(iter: number?): number
|
||||||
|
local n = iter or 1
|
||||||
|
assert(n > 0, "iterations must be greater than 0")
|
||||||
|
assert(bench, "no active benchmark")
|
||||||
|
assert(not bench.time_start, "clock was already started")
|
||||||
|
|
||||||
|
bench.iterations = n
|
||||||
|
bench.memory_start = gcinfo()
|
||||||
|
bench.time_start = os.clock()
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
local function BENCH(name: string, fn: () -> ())
|
||||||
|
local active = bench
|
||||||
|
assert(not active, "a benchmark is already in progress")
|
||||||
|
|
||||||
|
bench = {}
|
||||||
|
assert(bench);
|
||||||
|
(collectgarbage :: any)("collect")
|
||||||
|
|
||||||
|
local mem_start = gcinfo()
|
||||||
|
local time_start = os.clock()
|
||||||
|
local err_msg: string?
|
||||||
|
|
||||||
|
local success = xpcall(fn, function(m: string)
|
||||||
|
err_msg = m .. debug.traceback(nil, 2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local time_stop = os.clock()
|
||||||
|
local mem_stop = gcinfo()
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
|
||||||
|
print(color.gray(err_msg :: string))
|
||||||
|
else
|
||||||
|
time_start = bench.time_start or time_start
|
||||||
|
mem_start = bench.memory_start or mem_start
|
||||||
|
|
||||||
|
local n = bench.iterations or 1
|
||||||
|
local d, d_unit = convert_units("s", (time_stop - time_start) / n)
|
||||||
|
local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))
|
||||||
|
|
||||||
|
local function round(x: number): string
|
||||||
|
return x > 0 and x < 10 and (x - math.floor(x)) > 0 and string.format("%2.1f", x)
|
||||||
|
or string.format("%3.f", x)
|
||||||
|
end
|
||||||
|
|
||||||
|
print(
|
||||||
|
string.format(
|
||||||
|
`%s %s %s %s{WALL} %s`,
|
||||||
|
color.gray(round(d)),
|
||||||
|
d_unit,
|
||||||
|
color.gray(round(a)),
|
||||||
|
a_unit,
|
||||||
|
color.gray(name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
bench = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Printing
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function print2(v: unknown)
|
||||||
|
type Buffer = { n: number, [number]: string }
|
||||||
|
type Cyclic = { n: number, [{}]: number }
|
||||||
|
|
||||||
|
-- overkill concatenationless string buffer
|
||||||
|
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
|
||||||
|
local TAB = " "
|
||||||
|
local indent = table.concat(table.create(stack, TAB))
|
||||||
|
|
||||||
|
if type(value) == "string" then
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = '"'
|
||||||
|
str[n + 2] = value
|
||||||
|
str[n + 3] = '"'
|
||||||
|
str.n = n + 3
|
||||||
|
elseif type(value) ~= "table" then
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = value == nil and "nil" or tostring(value)
|
||||||
|
str.n = n + 1
|
||||||
|
elseif next(value) == nil then
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = "{}"
|
||||||
|
str.n = n + 1
|
||||||
|
else -- is table
|
||||||
|
local tabbed_indent = indent .. TAB
|
||||||
|
|
||||||
|
if cyclic[value] then
|
||||||
|
str.n += 1
|
||||||
|
str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
|
||||||
|
return
|
||||||
|
else
|
||||||
|
cyclic.n += 1
|
||||||
|
cyclic[value] = cyclic.n
|
||||||
|
end
|
||||||
|
|
||||||
|
str.n += 3
|
||||||
|
str[str.n - 2] = "{ "
|
||||||
|
str[str.n - 1] = color.gray(tostring(cyclic[value]))
|
||||||
|
str[str.n - 0] = "\n"
|
||||||
|
|
||||||
|
local i, v = next(value, nil)
|
||||||
|
while v ~= nil do
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = tabbed_indent
|
||||||
|
|
||||||
|
if type(i) ~= "string" then
|
||||||
|
str[n + 2] = "["
|
||||||
|
str[n + 3] = tostring(i)
|
||||||
|
str[n + 4] = "]"
|
||||||
|
n += 4
|
||||||
|
else
|
||||||
|
str[n + 2] = tostring(i)
|
||||||
|
n += 2
|
||||||
|
end
|
||||||
|
|
||||||
|
str[n + 1] = " = "
|
||||||
|
str.n = n + 1
|
||||||
|
|
||||||
|
tos(v, stack + 1, str, cyclic)
|
||||||
|
|
||||||
|
i, v = next(value, i)
|
||||||
|
|
||||||
|
n = str.n
|
||||||
|
str[n + 1] = v ~= nil and ",\n" or "\n"
|
||||||
|
str.n = n + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local n = str.n
|
||||||
|
str[n + 1] = indent
|
||||||
|
str[n + 2] = "}"
|
||||||
|
str.n = n + 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local str = { n = 0 }
|
||||||
|
local cyclic = { n = 0 }
|
||||||
|
tos(v, 0, str, cyclic)
|
||||||
|
print(table.concat(str))
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Equality
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function shallow_eq(a: {}, b: {}): boolean
|
||||||
|
if #a ~= #b then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in next, a do
|
||||||
|
if b[i] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in next, b do
|
||||||
|
if a[i] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function deep_eq(a: {}, b: {}): boolean
|
||||||
|
if #a ~= #b then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in next, a do
|
||||||
|
if type(b[i]) == "table" and type(v) == "table" then
|
||||||
|
if deep_eq(b[i], v) == false then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
elseif b[i] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in next, b do
|
||||||
|
if type(a[i]) == "table" and type(v) == "table" then
|
||||||
|
if deep_eq(a[i], v) == false then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
elseif a[i] ~= v then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Return
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
test = function()
|
||||||
|
return TEST, CASE, CHECK, FINISH, SKIP, FOCUS
|
||||||
|
end,
|
||||||
|
|
||||||
|
benchmark = function()
|
||||||
|
return BENCH, START
|
||||||
|
end,
|
||||||
|
|
||||||
|
disable_formatting = function()
|
||||||
|
disable_ansi = true
|
||||||
|
end,
|
||||||
|
|
||||||
|
print = print2,
|
||||||
|
|
||||||
|
seq = shallow_eq,
|
||||||
|
deq = deep_eq,
|
||||||
|
|
||||||
|
color = color,
|
||||||
|
}
|
18
wally.toml
Normal file
18
wally.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "mark-marks/jecs-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
|
realm = "shared"
|
||||||
|
license = "MIT"
|
||||||
|
exclude = ["**"]
|
||||||
|
include = [
|
||||||
|
"default.project.json",
|
||||||
|
"lib",
|
||||||
|
"lib/**",
|
||||||
|
"LICENSE",
|
||||||
|
"wally.toml",
|
||||||
|
"README.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jecs = "ukendio/jecs@0.2.10"
|
Loading…
Add table
Add a link
Reference in a new issue