Compare commits

...

6 commits

Author SHA1 Message Date
Clown
f6312a8b6f
Merge 96bed9bd7e into 58e67eda0d 2025-03-26 17:00:44 -07:00
unknown
58e67eda0d Target should return the entity with its generation
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-26 14:23:54 +01:00
Ukendio
045408af37 Rollback types
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-26 04:59:11 +01:00
Ukendio
59e7fd1f41 Fix nth count for target 2025-03-26 03:39:04 +01:00
Ukendio
f3befa3adb Add dual types 2025-03-25 23:13:53 +01:00
YetAnotherClown
96bed9bd7e Empty Commit 2025-02-25 11:28:04 -05:00
31 changed files with 1390 additions and 427 deletions

View file

@ -0,0 +1,22 @@
---
name: Bug report
about: File a bug report for any behavior that you believe is unintentional or problematic
title: "[BUG]"
labels: bug
assignees: ''
---
## Describe the bug
Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps.
## Reproduction
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
## Expected Behavior
What you expect to happen
## Actual Behavior
What actually happens

View file

@ -0,0 +1,14 @@
---
name: Documentation
about: Open an issue to add, change, or otherwise modify any part of the documentation.
title: "[DOCS]"
labels: documentation
assignees: ''
---
## Which Sections Does This Issue Cover?
[Put sections (e.g. Query Concepts), page links, etc as necessary]
## What Needs To Change?
What specifically needs to change and what suggestions do you have to change it?

View file

@ -0,0 +1,27 @@
---
name: Feature Request
about: File a feature request for something you believe should be added to Jecs
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
## Describe your Feature
You should explain your feature here, and the motivation for why you want it.
## Implementation
Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable).
## Alternatives
What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation?
## Considerations
Some questions that need to be answered include the following:
- Will old code break in response to this feature?
- What are the performance impacts with this feature (if any)?
- How is it useful to include?

15
assets/.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,15 @@
## Brief Description of your Changes.
Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
## Impact of your Changes
What implications will this have on the project? Will there be altered behavior or performance with this change?
## Tests Performed
What have you done to ensure this change has the least possible impact on the project?
## Additional Comments
Anything else you feel is relevant.

19
assets/.github/workflows/analysis.yaml vendored Normal file
View file

@ -0,0 +1,19 @@
name: analysis
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Luau Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v2.1
- name: Analyze
run: |
output=$(luau-analyze src || true) # Suppress errors for now.

11
assets/.github/workflows/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: npm
directory: "/"
schedule:
interval: "daily"

View file

@ -0,0 +1,64 @@
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: deploy-docs
on:
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci # or pnpm install / yarn install / bun install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -0,0 +1,17 @@
name: publish-npm
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "20"
- uses: JS-DevTools/npm-publish@v3
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}

71
assets/.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,71 @@
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: Install Dependencies
run: wally install
- name: Build
run: rojo build --output build.rbxm default.project.json
- 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 Jecs Build
uses: actions/download-artifact@v3
with:
name: build
path: build
- name: Rename Build
run: mv build/build.rbxm jecs.rbxm
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: Jecs ${{ github.ref_name }}
files: |
jecs.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

View file

@ -0,0 +1,31 @@
name: unit-testing
on: [push, pull_request, workflow_dispatch]
jobs:
run:
name: Run Luau Tests
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v4.3
with:
version: "latest"
verbose: "true"
- name: Run Unit Tests
id: run_tests
run: |
output=$(luau test/tests.luau)
echo "$output"
if [[ "$output" == *"0 fails"* ]]; then
echo "Unit Tests Passed"
else
echo "Error: One or More Unit Tests Failed."
exit 1
fi

827
jecs.luau

File diff suppressed because it is too large Load diff

View file

@ -5,21 +5,21 @@ local lifetime_tracker_add = require("@tools/lifetime_tracker")
local pe = require("@tools/entity_visualiser").prettify
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
local FriendsWith = world:component()
local _1 = world:print_snapshot()
world:print_snapshot()
local e1 = world:entity()
local e2 = world:entity()
world:delete(e2)
local _2 = world:print_snapshot()
world:print_snapshot()
local e3 = world:entity()
world:add(e3, pair(ChildOf, e1))
local e4 = world:entity()
world:add(e4, pair(FriendsWith, e3))
local _3 = world:print_snapshot()
world:print_snapshot()
world:delete(e1)
world:delete(e3)
local _4 = world:print_snapshot()
world:print_snapshot()
world:print_entity_index()
world:entity()
world:entity()
local _5 = world:print_snapshot()
world:print_snapshot()

11
test/examples/README.md Normal file
View file

@ -0,0 +1,11 @@
# Examples
This folder contains code examples for the Luau/Typescript APIs.
## Run with Luau
To run the examples with Luau, run the following commands from the root of the repository:
```sh
cd examples/luau
luau path/to/file.luau
```

View file

@ -0,0 +1,43 @@
local jecs = require("@jecs")
local world = jecs.World.new()
local Position = world:component()
local Walking = world:component()
local Name = world:component()
-- Create an entity with name Bob
local bob = world:entity()
-- The set operation finds or creates a component, and sets it.
world:set(bob, Position, Vector3.new(10, 20, 30))
-- Name the entity Bob
world:set(bob, Name, "Bob")
-- The add operation adds a component without setting a value. This is
-- useful for tags, or when adding a component with its default value.
world:add(bob, Walking)
-- Get the value for the Position component
local pos = world:get(bob, Position)
print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`)
-- Overwrite the value of the Position component
world:set(bob, Position, Vector3.new(40, 50, 60))
local alice = world:entity()
-- Create another named entity
world:set(alice, Name, "Alice")
world:set(alice, Position, Vector3.new(10, 20, 30))
world:add(alice, Walking)
-- Remove tag
world:remove(alice, Walking)
-- Iterate all entities with Position
for entity, p in world:query(Position) do
print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`)
end
-- Output:
-- {10, 20, 30}
-- Alice: {10, 20, 30}
-- Bob: {40, 50, 60}

View file

@ -0,0 +1,112 @@
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local world = jecs.World.new()
local Name = world:component()
local Position = world:component()
local Star = world:component()
local Planet = world:component()
local Moon = world:component()
local Vector3
do
Vector3 = {}
Vector3.__index = Vector3
function Vector3.new(x, y, z)
x = x or 0
y = y or 0
z = z or 0
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
end
function Vector3.__add(left, right)
return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z)
end
function Vector3.__mul(left, right)
if typeof(right) == "number" then
return Vector3.new(left.X * right, left.Y * right, left.Z * right)
end
return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z)
end
Vector3.one = Vector3.new(1, 1, 1)
Vector3.zero = Vector3.new()
end
local function path(entity)
local str = world:get(entity, Name)
local parent
while true do
parent = world:parent(entity)
if not parent then
break
end
entity = parent
str = world:get(parent, Name) .. "/" .. str
end
return str
end
local function iterate(entity, parent)
local p = world:get(entity, Position)
local actual = p + parent
print(path(entity))
print(`\{{actual.X}, {actual.Y}, {actual.Z}}`)
for child in world:query(pair(ChildOf, entity)) do
--print(world:get(child, Name))
iterate(child, actual)
end
end
local sun = world:entity()
world:add(sun, Star)
world:set(sun, Position, Vector3.one)
world:set(sun, Name, "Sun")
do
local earth = world:entity()
world:set(earth, Name, "Earth")
world:add(earth, pair(ChildOf, sun))
world:add(earth, Planet)
world:set(earth, Position, Vector3.one * 3)
do
local moon = world:entity()
world:set(moon, Name, "Moon")
world:add(moon, pair(ChildOf, earth))
world:add(moon, Moon)
world:set(moon, Position, Vector3.one * 0.1)
print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`)
end
local venus = world:entity()
world:set(venus, Name, "Venus")
world:add(venus, pair(ChildOf, sun))
world:add(venus, Planet)
world:set(venus, Position, Vector3.one * 2)
local mercury = world:entity()
world:set(mercury, Name, "Mercury")
world:add(mercury, pair(ChildOf, sun))
world:add(mercury, Planet)
world:set(mercury, Position, Vector3.one)
iterate(sun, Vector3.zero)
end
-- Output:
-- Child of Earth? true
-- Sun
-- {1, 1, 1}
-- Sun/Mercury
-- {2, 2, 2}
-- Sun/Venus
-- {3, 3, 3}
-- Sun/Earth
-- {4, 4, 4}
-- Sun/Earth/Moon
-- {4.1, 4.1, 4.1}

View file

@ -0,0 +1,21 @@
local jecs = require("@jecs")
local world = jecs.World.new()
local Model = world:component()
-- It is important to define hooks for the component before the component is ever used
-- otherwise the hooks will never invoke!
world:set(Model, jecs.OnRemove, function(entity)
-- OnRemove is invoked before the component and its value is removed
-- which provides a stable reference to the entity at deletion.
-- This means that it is safe to retrieve the data inside of a hook
local model = world:get(entity, Model)
model:Destroy()
end)
world:set(Model, jecs.OnSet, function(entity, model)
-- OnSet is invoked after the data has been assigned.
-- It also returns the data for faster access.
-- There may be some logic to do some side effects on reassignments
model:SetAttribute("entityId", entity)
end)

View file

@ -0,0 +1,63 @@
local jecs = require("@jecs")
local world = jecs.World.new()
local Position = world:component()
local Velocity = world:component()
local Name = world:component()
local Vector3
do
Vector3 = {}
Vector3.__index = Vector3
function Vector3.new(x, y, z)
x = x or 0
y = y or 0
z = z or 0
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
end
function Vector3.__add(left, right)
return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z)
end
function Vector3.__mul(left, right)
if typeof(right) == "number" then
return Vector3.new(left.X * right, left.Y * right, left.Z * right)
end
return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z)
end
Vector3.one = Vector3.new(1, 1, 1)
Vector3.zero = Vector3.new()
end
-- Create a few test entities for a Position, Velocity query
local e1 = world:entity()
world:set(e1, Name, "e1")
world:set(e1, Position, Vector3.new(10, 20, 30))
world:set(e1, Velocity, Vector3.new(1, 2, 3))
local e2 = world:entity()
world:set(e2, Name, "e2")
world:set(e2, Position, Vector3.new(10, 20, 30))
world:set(e2, Velocity, Vector3.new(4, 5, 6))
-- This entity will not match as it does not have Position, Velocity
local e3 = world:entity()
world:set(e3, Name, "e3")
world:set(e3, Position, Vector3.new(10, 20, 30))
-- Create an uncached query for Position, Velocity.
for entity, p, v in world:query(Position, Velocity) do
-- Iterate entities matching the query
p.X += v.X
p.Y += v.Y
p.Z += v.Z
print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`)
end
-- Output:
-- e2: {14, 25, 36}
-- e1: {11, 22, 33}

View file

@ -0,0 +1,61 @@
local jecs = require("@jecs")
local world = jecs.World.new()
local Name = world:component()
local function named(ctr, name)
local e = ctr(world)
world:set(e, Name, name)
return e
end
local function name(e)
return world:get(e, Name)
end
local Position = named(world.component, "Position") :: jecs.Entity<Vector3>
local Previous = jecs.Rest
local PreviousPosition = jecs.pair(Previous, Position)
local added = world
:query(Position)
:without(PreviousPosition)
:cached()
local changed = world
:query(Position, PreviousPosition)
:cached()
local removed = world
:query(PreviousPosition)
:without(Position)
:cached()
local e1 = named(world.entity, "e1")
world:set(e1, Position, Vector3.new(10, 20, 30))
local e2 = named(world.entity, "e2")
world:set(e2, Position, Vector3.new(10, 20, 30))
for e, p in added:iter() do
print(`Added {e}: \{{p.X}, {p.Y}, {p.Z}}`)
world:set(e, PreviousPosition, p)
end
world:set(e1, Position, "")
for e, new, old in changed:iter() do
if new ~= old then
print(`{name(new)}'s Position changed from \{{old.X}, {old.Y}, {old.Z}\} to \{{new.X}, {new.Y}, {new.Z}\}`)
world:set(e, PreviousPosition, new)
end
end
world:remove(e2, Position)
for e in removed:iter() do
print(`Position was removed from {name(e)}`)
end
-- Output:
-- Added 265: {10, 20, 30}
-- Added 264: {10, 20, 30}
-- e1's Position changed from {10, 20, 30} to {999, 999, 1998}
-- Position was removed from e2

View file

@ -0,0 +1,125 @@
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard
local Name = jecs.Name
local world = jecs.World.new()
type Id<T = nil> = number & { __T: T }
local Voxel = world:component() :: Id
local Position = world:component() :: Id<Vector3>
local Perception = world:component() :: Id<{
range: number,
fov: number,
dir: Vector3,
}>
local PrimaryPart = world:component() :: Id<Part>
local local_player = game:GetService("Players").LocalPlayer
local function distance(a: Vector3, b: Vector3)
return (b - a).Magnitude
end
local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number)
local to_target = b - a
local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit
local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit
local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X))
local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X))
local angle_difference = math.abs(forward_angle - angle_to_target)
if angle_difference > 180 then
angle_difference = 360 - angle_difference
end
return angle_difference <= (fov_angle / 2)
end
local map = {}
local grid = 50
local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?)
local hash = position // grid
local voxel_id = map[hash]
if not voxel_id then
voxel_id = world:entity()
world:add(voxel_id, Voxel)
world:set(voxel_id, Position, hash)
map[hash] = voxel_id
end
if prev_voxel_id ~= nil then
world:remove(source, pair(ChildOf, prev_voxel_id))
end
world:add(source, pair(ChildOf, voxel_id))
end
local function reconcile_client_owned_assembly_to_voxel(dt: number)
for e, part, position in world:query(PrimaryPart, Position) do
local p = part.Position
if p ~= position then
world:set(e, Position, p)
local voxel_id = world:target(e, ChildOf, 0)
if map[p // grid] == voxel_id then
continue
end
add_to_voxel(e, p, voxel_id)
end
end
end
local function update_camera_direction(dt: number)
for _, perception in world:query(Perception) do
perception.dir = workspace.CurrentCamera.CFrame.LookVector
end
end
local function perceive_enemies(dt: number)
local it = world:query(Perception, Position, PrimaryPart)
-- There is only going to be one entity matching the query
local e, self_perception, self_position, self_primary_part = it()
local voxel_id = map[self_primary_part.Position // grid]
local nearby_entities_query = world:query(Position, pair(ChildOf, voxel_id))
for enemy, target_position in nearby_entities_query do
if distance(self_position, target_position) > self_perception.range then
continue
end
if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then
local p = target_position
print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`)
end
end
end
local player = world:entity()
world:set(player, Perception, {
range = 100,
fov = 90,
dir = Vector3.new(1, 0, 0),
})
world:set(player, Name, "LocalPlayer")
local primary_part = (local_player.Character :: Model).PrimaryPart :: Part
world:set(player, PrimaryPart, primary_part)
world:set(player, Position, Vector3.zero)
local enemy = world:entity()
world:set(enemy, Name, "Enemy $1")
world:set(enemy, Position, Vector3.new(50, 0, 20))
add_to_voxel(player, primary_part.Position)
add_to_voxel(enemy, world)
local dt = 1 / 60
reconcile_client_owned_assembly_to_voxel(dt)
update_camera_direction(dt)
perceive_enemies(dt)
-- Output:
-- LocalPlayer can see target Enemy $1

View file

@ -0,0 +1,37 @@
local jecs = require("@jecs")
local pair = jecs.pair
local world = jecs.World.new()
local Name = world:component()
local function named(ctr, name)
local e = ctr(world)
world:set(e, Name, name)
return e
end
local function name(e)
return world:get(e, Name)
end
local Eats = world:component()
local Apples = named(world.entity, "Apples")
local Oranges = named(world.entity, "Oranges")
local bob = named(world.entity, "Bob")
world:set(bob, pair(Eats, Apples), 10)
local alice = named(world.entity, "Alice")
world:set(alice, pair(Eats, Oranges), 5)
-- Aliasing the wildcard to symbols improves readability and ease of writing
local __ = jecs.Wildcard
-- Create a query that matches edible components
for entity, amount in world:query(pair(Eats, __)) do
-- Iterate the query
local food = world:target(entity, Eats)
print(`{name(entity)} eats {amount} {name(food)}`)
end
-- Output:
-- Alice eats 5 Oranges
-- Bob eats 10 Apples

View file

@ -117,6 +117,78 @@ local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("#adding a recycled target", function()
local world = world_new()
local R = world:component()
local e = world:entity()
local T = world:entity()
world:add(e, pair(R, T))
world:delete(T)
CHECK(not world:has(e, pair(R, T)))
local T2 = world:entity()
world:add(e, pair(R, T2))
CHECK(world:target(e, R) ~= T)
CHECK(world:target(e, R) ~= 0)
end)
TEST("#repro2", function()
local world = world_new()
local Lifetime = world:component() :: jecs.Id<number>
local Particle = world:entity()
local Beam = world:entity()
local entity = world:entity()
world:set(entity, pair(Lifetime, Particle), 1)
world:set(entity, pair(Lifetime, Beam), 2)
world:set(entity, pair(4, 5), 6) -- noise
local entity_visualizer = require("@tools/entity_visualiser")
entity_visualizer.components(world, entity)
for e in world:each(pair(Lifetime, __)) do
local i = 0
local nth = world:target(e, Lifetime, i)
while nth do
entity_visualizer.components(world, e)
local data = world:get(e, pair(Lifetime, nth))
data -= 1
if data <= 0 then
world:remove(e, pair(Lifetime, nth))
else
world:set(e, pair(Lifetime, nth), data)
end
i += 1
nth = world:target(e, Lifetime, i)
end
end
CHECK(not world:has(entity, pair(Lifetime, Particle)))
CHECK(world:get(entity, pair(Lifetime, Beam)) == 1)
end)
local lifetime_tracker_add = require("@tools/lifetime_tracker")
TEST("another", function()
local world = world_new()
world = lifetime_tracker_add(world, {padding_enabled=false})
local e1 = world:entity()
local e2 = world:entity()
local e3 = world:entity()
world:delete(e2)
world:print_entity_index()
print(pair(e1, e2))
print(pair(e2, e3))
local e2_e3 = pair(e2, e3)
CHECK(jecs.pair_first(world, e2_e3) == 0)
CHECK(jecs.pair_second(world, e2_e3) == e3)
CHECK_EXPECT_ERR(function()
world:add(e1, pair(e2, e3))
end)
end)
TEST("#repro", function()
local world = world_new()
@ -186,9 +258,9 @@ end)
TEST("world:cleanup()", function()
local world = world_new()
local A = world:component()
local B = world:component()
local C = world:component()
local A = world:component() :: jecs.Id<boolean>
local B = world:component() :: jecs.Id<boolean>
local C = world:component() :: jecs.Id<boolean>
local e1 = world:entity()
local e2 = world:entity()
@ -299,7 +371,7 @@ TEST("world:entity()", function()
do CASE "Recycling max generation"
local world = world_new()
local pin = jecs.Rest::number + 1
local pin = (jecs.Rest :: any) :: number + 1
for i = 1, 2^16-1 do
local e = world:entity()
world:delete(e)
@ -513,21 +585,22 @@ TEST("world:query()", function()
do CASE "pairs"
local world = jecs.World.new()
local C1 = world:component()
local C2 = world:component()
local C1 = world:component() :: jecs.Id<boolean>
local C2 = world:component() :: jecs.Id<boolean>
local T1 = world:entity()
local T2 = world:entity()
local e = world:entity()
world:set(e, pair(C1, C2), true)
local C1_C2 = pair(C1, C2)
world:set(e, C1_C2, true)
world:set(e, pair(C1, T1), true)
world:set(e, pair(T1, C1), true)
CHECK_EXPECT_ERR(function()
world:set(e, pair(T1, T2), true :: any)
end)
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) :: any do
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)):iter() do
CHECK(a == true)
CHECK(b == true)
CHECK(c == true)
@ -823,8 +896,9 @@ TEST("world:query()", function()
local bob = world:entity()
world:delete(Apples)
world:set(bob, pair(Eats, Apples), "bob eats apples")
CHECK_EXPECT_ERR(function()
world:set(bob, pair(Eats, Apples), "bob eats apples")
end)
end
do
@ -1394,24 +1468,10 @@ TEST("world:target", function()
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
-- for id in archetype.records do
-- local f = world:get(ecs_pair_first(world, id), jecs.Name)
-- local s = world:get(ecs_pair_second(world, id), jecs.Name)
-- print(`({f}, {s})`)
-- end
--
CHECK(archetype.records[pair(A, B)] == 1)
CHECK(archetype.records[pair(A, C)] == 2)
CHECK(archetype.records[pair(A, D)] == 3)
CHECK(archetype.records[pair(A, E)] == 4)
-- print("(A, B)", archetype.records[pair(A, B)])
-- print("(A, C)", archetype.records[pair(A, C)])
-- print("(A, D)", archetype.records[pair(A, D)])
-- print("(A, E)", archetype.records[pair(A, E)])
-- print(pair(A, D), pair(B, C))
-- print("(B, C)", archetype.records[pair(B, C)])
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

108
tools/runtime_lints.luau Normal file
View file

@ -0,0 +1,108 @@
local function dbg_info(n: number): any
return debug.info(n, "s")
end
local function throw(msg: string)
local s = 1
local root = dbg_info(1)
repeat
s += 1
until dbg_info(s) ~= root
if warn then
error(msg, s)
else
print(`[jecs] error: {msg}\n`)
end
end
local function ASSERT<T>(v: T, msg: string)
if v then
return
end
throw(msg)
end
local function runtime_lints_add(world)
local function get_name(id)
return world_get_one_inline(world, id, EcsName)
end
local function bname(id): string
local name: string
if ECS_IS_PAIR(id) then
local first = get_name(world, ecs_pair_first(world, id))
local second = get_name(world, ecs_pair_second(world, id))
name = `pair({first}, {second})`
else
return get_name(world, id)
end
if name then
return name
else
return `${id}`
end
end
local function ID_IS_TAG(world: World, id)
if ECS_IS_PAIR(id) then
id = ecs_pair_first(world, id)
end
return not world_has_one_inline(world, id, EcsComponent)
end
World.query = function(world: World, ...)
ASSERT((...), "Requires at least a single component")
return world_query(world, ...)
end
World.set = function(world: World, entity: i53, id: i53, value: any): ()
local is_tag = ID_IS_TAG(world, id)
if is_tag and value == nil then
local _1 = bname(world, entity)
local _2 = bname(world, id)
local why = "cannot set component value to nil"
throw(why)
return
elseif value ~= nil and is_tag then
local _1 = bname(world, entity)
local _2 = bname(world, id)
local why = `cannot set a component value because {_2} is a tag`
why ..= `\n[jecs] note: consider using "world:add({_1}, {_2})" instead`
throw(why)
return
end
world_set(world, entity, id, value)
end
World.add = function(world: World, entity: i53, id: i53, value: any)
if value ~= nil then
local _1 = bname(world, entity)
local _2 = bname(world, id)
throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`)
end
world_add(world, entity, id)
end
World.get = function(world: World, entity: i53, ...)
local length = select("#", ...)
ASSERT(length < 5, "world:get does not support more than 4 components")
local _1
for i = 1, length do
local id = select(i, ...)
local id_is_tag = not world_has(world, id, EcsComponent)
if id_is_tag then
local name = get_name(world, id)
if not _1 then
_1 = get_name(world, entity)
end
throw(
`cannot get (#{i}) component {name} value because it is a tag.`
.. `\n[jecs] note: If this was intentional, use "world:has({_1}, {name}) instead"`
)
end
end
return world_get(world, entity, ...)
end
end