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