Cleanup & refactor
Some checks failed
Continous Integration / Build (push) Successful in 11s
Continous Integration / Lint (push) Successful in 9s
Continous Integration / Styling (push) Failing after 3s
Continous Integration / Unit Testing (push) Failing after 30s

This commit is contained in:
marked 2025-05-07 00:37:24 +02:00
parent d3b6212463
commit 2a6907434a
50 changed files with 937 additions and 4110 deletions

View file

@ -1,17 +0,0 @@
{
"process": [
{
"rule": "convert_require",
"current": {
"name": "path",
"sources": {
"@jecs": "Packages/jecs"
}
},
"target": {
"name": "roblox",
"rojo_sourcemap": "sourcemap.json"
}
}
]
}

98
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,98 @@
name: Continous Integration
on:
push:
pull_request:
branches:
- main
jobs:
build:
name: Build
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: https://github.com/CompeyDev/setup-rokit@v0.1.2
with:
token: ${{ secrets.githubtoken }}
- name: Build
run: |
lune run build
- name: Upload Build Artifact
uses: https://git.devmarked.win/actions/upload-artifact@v4
with:
name: build
path: hammer.rbxm
lint:
name: Lint
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rokit
uses: https://github.com/CompeyDev/setup-rokit@v0.1.2
with:
token: ${{ secrets.githubtoken }}
- name: Lint
run: |
selene lib/
style:
name: Styling
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Check code style
uses: https://github.com/JohnnyMorganz/stylua-action@v4
with:
token: ${{ secrets.githubtoken }}
version: v2.1.0
args: --check lib/
test:
name: Unit Testing
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Luau
uses: https://github.com/EncodedVenom/install-luau@v4
- name: Install Rokit
uses: https://github.com/CompeyDev/setup-rokit@v0.1.2
with:
token: ${{ secrets.githubtoken }}
- name: Install Packages
run: |
pesde install
- name: Run Unit 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

View file

@ -7,28 +7,34 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
uses: https://github.com/CompeyDev/setup-rokit@v0.1.2
with:
token: ${{ secrets.githubtoken }}
- name: Build
run: |
lune run build
- name: Upload Build Artifact
uses: actions/upload-artifact@v3
uses: https://git.devmarked.win/actions/upload-artifact@v4
with:
name: build
path: build.rbxm
path: hammer.rbxm
release:
name: Release
needs: [build]
runs-on: ubuntu-latest
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
permissions:
contents: write
steps:
@ -36,35 +42,32 @@ jobs:
uses: actions/checkout@v4
- name: Download Build
uses: actions/download-artifact@v3
uses: https://git.devmarked.win/actions/download-artifact@v4
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
uses: actions/forgejo-release@v2
with:
name: Jecs Utils ${{ github.ref_name }}
files: |
jecs_utils.rbxm
direction: upload
title: Hammer ${{ github.ref_name }}
release-dir: build
publish:
name: Publish
needs: [release]
runs-on: ubuntu-latest
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-24.04
steps:
- name: Checkout Project
uses: actions/checkout@v4
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
- name: Prepare for Distribution
run: |
lune run dist
uses: https://github.com/CompeyDev/setup-rokit@v0.1.2
with:
token: ${{ secrets.githubtoken }}
- name: Wally Login
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}

View file

@ -1,90 +0,0 @@
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 lib/
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 lib/
test:
name: Unit Testing
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Luau
uses: encodedvenom/install-luau@v2.1
- name: Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Packages
run: |
pesde install
# Not needed anymore thanks to pesde 🥳
#- name: Download Jecs
# run: |
# lune run download-jecs ${{ secrets.GITHUB_TOKEN }}
- name: Run Unit 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

View file

@ -1,7 +1,7 @@
{
"languageMode": "strict",
"aliases": {
"lib": "lib",
"pkg": "luau_packages"
"pkg": "luau_packages",
"lune": "~/.lune/.typedefs/0.9.2"
}
}

View file

@ -1,408 +0,0 @@
--[[
NOTE: We export a couple different DateTimeValues types below to ensure
that types are completely accurate, for method args milliseconds will
always be optional, but for return values millis are always included
If we figure out some better strategy here where we can
export just a single type while maintaining accuracy we
can change to that in a future breaking semver release
]]
type OptionalMillisecond = {
millisecond: number?,
}
type Millisecond = {
millisecond: number,
}
--[=[
@interface Locale
@within DateTime
Enum type representing supported DateTime locales.
Currently supported locales are:
- `en` - English
- `de` - German
- `es` - Spanish
- `fr` - French
- `it` - Italian
- `ja` - Japanese
- `pl` - Polish
- `pt-br` - Brazilian Portuguese
- `pt` - Portuguese
- `tr` - Turkish
]=]
export type Locale = "en" | "de" | "es" | "fr" | "it" | "ja" | "pl" | "pt-br" | "pt" | "tr"
--[=[
@interface DateTimeValues
@within DateTime
Individual date & time values, representing the primitives that make up a `DateTime`.
This is a dictionary that will contain the following values:
- `year` - Year(s), in the range 1400 -> 9999
- `month` - Month(s), in the range 1 -> 12
- `day` - Day(s), in the range 1 -> 31
- `hour` - Hour(s), in the range 0 -> 23
- `minute` - Minute(s), in the range 0 -> 59
- `second` - Second(s), in the range 0 -> 60, where 60 is a leap second
An additional `millisecond` value may also be included,
and should be within the range `0 -> 999`, but is optional.
However, any method returning this type should be guaranteed
to include milliseconds - see individual methods to verify.
]=]
export type DateTimeValues = {
year: number,
month: number,
day: number,
hour: number,
minute: number,
second: number,
}
--[=[
@interface DateTimeValueArguments
@within DateTime
Alias for `DateTimeValues` with an optional `millisecond` value.
Refer to the `DateTimeValues` documentation for additional information.
]=]
export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond
--[=[
@interface DateTimeValueReturns
@within DateTime
Alias for `DateTimeValues` with a mandatory `millisecond` value.
Refer to the `DateTimeValues` documentation for additional information.
]=]
export type DateTimeValueReturns = DateTimeValues & Millisecond
local DateTime = {
--- Number of seconds passed since the UNIX epoch.
unixTimestamp = (nil :: any) :: number,
--- Number of milliseconds passed since the UNIX epoch.
unixTimestampMillis = (nil :: any) :: number,
}
--[=[
@within DateTime
@tag Method
Formats this `DateTime` using the given `formatString` and `locale`, as local time.
The given `formatString` is parsed using a `strftime`/`strptime`-inspired
date and time formatting syntax, allowing tokens such as the following:
| Token | Example | Description |
|-------|----------|---------------|
| `%Y` | `1998` | Year number |
| `%m` | `04` | Month number |
| `%d` | `29` | Day number |
| `%A` | `Monday` | Weekday name |
| `%M` | `59` | Minute number |
| `%S` | `10` | Second number |
For a full reference of all available tokens, see the
[chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
If not provided, `formatString` and `locale` will default
to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively.
@param formatString -- A string containing formatting tokens
@param locale -- The locale the time should be formatted in
@return string -- The formatting string
]=]
function DateTime.formatLocalTime(self: DateTime, formatString: string?, locale: Locale?): string
return nil :: any
end
--[=[
@within DateTime
@tag Method
Formats this `DateTime` using the given `formatString` and `locale`, as UTC (universal) time.
The given `formatString` is parsed using a `strftime`/`strptime`-inspired
date and time formatting syntax, allowing tokens such as the following:
| Token | Example | Description |
|-------|----------|---------------|
| `%Y` | `1998` | Year number |
| `%m` | `04` | Month number |
| `%d` | `29` | Day number |
| `%A` | `Monday` | Weekday name |
| `%M` | `59` | Minute number |
| `%S` | `10` | Second number |
For a full reference of all available tokens, see the
[chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
If not provided, `formatString` and `locale` will default
to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively.
@param formatString -- A string containing formatting tokens
@param locale -- The locale the time should be formatted in
@return string -- The formatting string
]=]
function DateTime.formatUniversalTime(
self: DateTime,
formatString: string?,
locale: Locale?
): string
return nil :: any
end
--[=[
@within DateTime
@tag Method
Formats this `DateTime` as an ISO 8601 date-time string.
Some examples of ISO 8601 date-time strings are:
- `2020-02-22T18:12:08Z`
- `2000-01-31T12:34:56+05:00`
- `1970-01-01T00:00:00.055Z`
@return string -- The ISO 8601 formatted string
]=]
function DateTime.toIsoDate(self: DateTime): string
return nil :: any
end
--[=[
@within DateTime
@tag Method
Extracts separated local date & time values from this `DateTime`.
The returned table contains the following values:
| Key | Type | Range |
|---------------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
| `millisecond` | `number` | `0 -> 999` |
@return DateTimeValueReturns -- A table of DateTime values
]=]
function DateTime.toLocalTime(self: DateTime): DateTimeValueReturns
return nil :: any
end
--[=[
@within DateTime
@tag Method
Extracts separated UTC (universal) date & time values from this `DateTime`.
The returned table contains the following values:
| Key | Type | Range |
|---------------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
| `millisecond` | `number` | `0 -> 999` |
@return DateTimeValueReturns -- A table of DateTime values
]=]
function DateTime.toUniversalTime(self: DateTime): DateTimeValueReturns
return nil :: any
end
export type DateTime = typeof(DateTime)
--[=[
@class DateTime
Built-in library for date & time
### Example usage
```lua
local DateTime = require("@lune/datetime")
-- Creates a DateTime for the current exact moment in time
local now = DateTime.now()
-- Formats the current moment in time as an ISO 8601 string
print(now:toIsoDate())
-- Formats the current moment in time, using the local
-- time, the French locale, and the specified time string
print(now:formatLocalTime("%A, %d %B %Y", "fr"))
-- Returns a specific moment in time as a DateTime instance
local someDayInTheFuture = DateTime.fromLocalTime({
year = 3033,
month = 8,
day = 26,
hour = 16,
minute = 56,
second = 28,
millisecond = 892,
})
-- Extracts the current local date & time as separate values (same values as above table)
print(now:toLocalTime())
-- Returns a DateTime instance from a given float, where the whole
-- denotes the seconds and the fraction denotes the milliseconds
-- Note that the fraction for millis here is completely optional
DateTime.fromUnixTimestamp(871978212313.321)
-- Extracts the current universal (UTC) date & time as separate values
print(now:toUniversalTime())
```
]=]
local dateTime = {}
--[=[
@within DateTime
@tag Constructor
Returns a `DateTime` representing the current moment in time.
@return DateTime -- The new DateTime object
]=]
function dateTime.now(): DateTime
return nil :: any
end
--[=[
@within DateTime
@tag Constructor
Creates a new `DateTime` from the given UNIX timestamp.
This timestamp may contain both a whole and fractional part -
where the fractional part denotes milliseconds / nanoseconds.
Example usage of fractions:
- `DateTime.fromUnixTimestamp(123456789.001)` - one millisecond
- `DateTime.fromUnixTimestamp(123456789.000000001)` - one nanosecond
Note that the fractional part has limited precision down to exactly
one nanosecond, any fraction that is more precise will get truncated.
@param unixTimestamp -- Seconds passed since the UNIX epoch
@return DateTime -- The new DateTime object
]=]
function dateTime.fromUnixTimestamp(unixTimestamp: number): DateTime
return nil :: any
end
--[=[
@within DateTime
@tag Constructor
Creates a new `DateTime` from the given date & time values table, in universal (UTC) time.
The given table must contain the following values:
| Key | Type | Range |
|----------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
An additional `millisecond` value may also be included,
and should be within the range `0 -> 999`, but is optional.
Any non-integer values in the given table will be rounded down.
### Errors
This constructor is fallible and may throw an error in the following situations:
- Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year.
@param values -- Table containing date & time values
@return DateTime -- The new DateTime object
]=]
function dateTime.fromUniversalTime(values: DateTimeValueArguments): DateTime
return nil :: any
end
--[=[
@within DateTime
@tag Constructor
Creates a new `DateTime` from the given date & time values table, in local time.
The given table must contain the following values:
| Key | Type | Range |
|----------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
An additional `millisecond` value may also be included,
and should be within the range `0 -> 999`, but is optional.
Any non-integer values in the given table will be rounded down.
### Errors
This constructor is fallible and may throw an error in the following situations:
- Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year.
@param values -- Table containing date & time values
@return DateTime -- The new DateTime object
]=]
function dateTime.fromLocalTime(values: DateTimeValueArguments): DateTime
return nil :: any
end
--[=[
@within DateTime
@tag Constructor
Creates a new `DateTime` from an ISO 8601 date-time string.
### Errors
This constructor is fallible and may throw an error if the given
string does not strictly follow the ISO 8601 date-time string format.
Some examples of valid ISO 8601 date-time strings are:
- `2020-02-22T18:12:08Z`
- `2000-01-31T12:34:56+05:00`
- `1970-01-01T00:00:00.055Z`
@param isoDate -- An ISO 8601 formatted string
@return DateTime -- The new DateTime object
]=]
function dateTime.fromIsoDate(isoDate: string): DateTime
return nil :: any
end
return dateTime

View file

@ -1,289 +0,0 @@
local DateTime = require("./datetime")
type DateTime = DateTime.DateTime
export type MetadataKind = "file" | "dir" | "symlink"
--[=[
@interface MetadataPermissions
@within FS
Permissions for the given file or directory.
This is a dictionary that will contain the following values:
* `readOnly` - If the target path is read-only or not
]=]
export type MetadataPermissions = {
readOnly: boolean,
}
-- FIXME: We lose doc comments here below in Metadata because of the union type
--[=[
@interface Metadata
@within FS
Metadata for the given file or directory.
This is a dictionary that will contain the following values:
* `kind` - If the target path is a `file`, `dir` or `symlink`
* `exists` - If the target path exists
* `createdAt` - The timestamp represented as a `DateTime` object at which the file or directory was created
* `modifiedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last modified
* `accessedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last accessed
* `permissions` - Current permissions for the file or directory
Note that timestamps are relative to the unix epoch, and
may not be accurate if the system clock is not accurate.
]=]
export type Metadata = {
kind: MetadataKind,
exists: true,
createdAt: DateTime,
modifiedAt: DateTime,
accessedAt: DateTime,
permissions: MetadataPermissions,
} | {
kind: nil,
exists: false,
createdAt: nil,
modifiedAt: nil,
accessedAt: nil,
permissions: nil,
}
--[=[
@interface WriteOptions
@within FS
Options for filesystem APIs what write to files and/or directories.
This is a dictionary that may contain one or more of the following values:
* `overwrite` - If the target path should be overwritten or not, in the case that it already exists
]=]
export type WriteOptions = {
overwrite: boolean?,
}
--[=[
@class FS
Built-in library for filesystem access
### Example usage
```lua
local fs = require("@lune/fs")
-- Reading a file
local myTextFile: string = fs.readFile("myFileName.txt")
-- Reading entries (files & dirs) in a directory
for _, entryName in fs.readDir("myDirName") do
if fs.isFile("myDirName/" .. entryName) then
print("Found file " .. entryName)
elseif fs.isDir("myDirName/" .. entryName) then
print("Found subdirectory " .. entryName)
end
end
```
]=]
local fs = {}
--[=[
@within FS
@tag must_use
Reads a file at `path`.
An error will be thrown in the following situations:
* `path` does not point to an existing file.
* The current process lacks permissions to read the file.
* Some other I/O error occurred.
@param path The path to the file to read
@return The contents of the file
]=]
function fs.readFile(path: string): string
return nil :: any
end
--[=[
@within FS
@tag must_use
Reads entries in a directory at `path`.
An error will be thrown in the following situations:
* `path` does not point to an existing directory.
* The current process lacks permissions to read the contents of the directory.
* Some other I/O error occurred.
@param path The directory path to search in
@return A list of files & directories found
]=]
function fs.readDir(path: string): { string }
return {}
end
--[=[
@within FS
Writes to a file at `path`.
An error will be thrown in the following situations:
* The file's parent directory does not exist.
* The current process lacks permissions to write to the file.
* Some other I/O error occurred.
@param path The path of the file
@param contents The contents of the file
]=]
function fs.writeFile(path: string, contents: buffer | string) end
--[=[
@within FS
Creates a directory and its parent directories if they are missing.
An error will be thrown in the following situations:
* `path` already points to an existing file or directory.
* The current process lacks permissions to create the directory or its missing parents.
* Some other I/O error occurred.
@param path The directory to create
]=]
function fs.writeDir(path: string) end
--[=[
@within FS
Removes a file.
An error will be thrown in the following situations:
* `path` does not point to an existing file.
* The current process lacks permissions to remove the file.
* Some other I/O error occurred.
@param path The file to remove
]=]
function fs.removeFile(path: string) end
--[=[
@within FS
Removes a directory and all of its contents.
An error will be thrown in the following situations:
* `path` is not an existing and empty directory.
* The current process lacks permissions to remove the directory.
* Some other I/O error occurred.
@param path The directory to remove
]=]
function fs.removeDir(path: string) end
--[=[
@within FS
@tag must_use
Gets metadata for the given path.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `path`.
* Some other I/O error occurred.
@param path The path to get metadata for
@return Metadata for the path
]=]
function fs.metadata(path: string): Metadata
return nil :: any
end
--[=[
@within FS
@tag must_use
Checks if a given path is a file.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `path`.
* Some other I/O error occurred.
@param path The file path to check
@return If the path is a file or not
]=]
function fs.isFile(path: string): boolean
return nil :: any
end
--[=[
@within FS
@tag must_use
Checks if a given path is a directory.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `path`.
* Some other I/O error occurred.
@param path The directory path to check
@return If the path is a directory or not
]=]
function fs.isDir(path: string): boolean
return nil :: any
end
--[=[
@within FS
Moves a file or directory to a new path.
Throws an error if a file or directory already exists at the target path.
This can be bypassed by passing `true` as the third argument, or a dictionary of options.
Refer to the documentation for `WriteOptions` for specific option keys and their values.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `from` or write at `to`.
* The new path exists on a different mount point.
* Some other I/O error occurred.
@param from The path to move from
@param to The path to move to
@param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists
]=]
function fs.move(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end
--[=[
@within FS
Copies a file or directory recursively to a new path.
Throws an error if a file or directory already exists at the target path.
This can be bypassed by passing `true` as the third argument, or a dictionary of options.
Refer to the documentation for `WriteOptions` for specific option keys and their values.
An error will be thrown in the following situations:
* The current process lacks permissions to read at `from` or write at `to`.
* Some other I/O error occurred.
@param from The path to copy from
@param to The path to copy to
@param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists
]=]
function fs.copy(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end
return fs

View file

@ -1,123 +0,0 @@
--[=[
@interface CompileOptions
@within Luau
The options passed to the luau compiler while compiling bytecode.
This is a dictionary that may contain one or more of the following values:
* `optimizationLevel` - Sets the compiler option "optimizationLevel". Defaults to `1`.
* `coverageLevel` - Sets the compiler option "coverageLevel". Defaults to `0`.
* `debugLevel` - Sets the compiler option "debugLevel". Defaults to `1`.
Documentation regarding what these values represent can be found [here](https://github.com/Roblox/luau/blob/bd229816c0a82a8590395416c81c333087f541fd/Compiler/include/luacode.h#L13-L39).
]=]
export type CompileOptions = {
optimizationLevel: number?,
coverageLevel: number?,
debugLevel: number?,
}
--[=[
@interface LoadOptions
@within Luau
The options passed while loading a luau chunk from an arbitrary string, or bytecode.
This is a dictionary that may contain one or more of the following values:
* `debugName` - The debug name of the closure. Defaults to `luau.load(...)`.
* `environment` - A custom environment to load the chunk in. Setting a custom environment will deoptimize the chunk and forcefully disable codegen. Defaults to the global environment.
* `injectGlobals` - Whether or not to inject globals in the custom environment. Has no effect if no custom environment is provided. Defaults to `true`.
* `codegenEnabled` - Whether or not to enable codegen. Defaults to `false`.
]=]
export type LoadOptions = {
debugName: string?,
environment: { [string]: any }?,
injectGlobals: boolean?,
codegenEnabled: boolean?,
}
--[=[
@class Luau
Built-in library for generating luau bytecode & functions.
### Example usage
```lua
local luau = require("@lune/luau")
local bytecode = luau.compile("print('Hello, World!')")
local callableFn = luau.load(bytecode)
-- Additionally, we can skip the bytecode generation and load a callable function directly from the code itself.
-- local callableFn = luau.load("print('Hello, World!')")
callableFn()
```
Since luau bytecode is highly compressible, it may also make sense to compress it using the `serde` library
while transmitting large amounts of it.
]=]
local luau = {}
--[=[
@within Luau
Compiles sourcecode into Luau bytecode
An error will be thrown if the sourcecode given isn't valid Luau code.
### Example usage
```lua
local luau = require("@lune/luau")
-- Compile the source to some highly optimized bytecode
local bytecode = luau.compile("print('Hello, World!')", {
optimizationLevel = 2,
coverageLevel = 0,
debugLevel = 1,
})
```
@param source The string that will be compiled into bytecode
@param compileOptions The options passed to the luau compiler that will output the bytecode
@return luau bytecode
]=]
function luau.compile(source: string, compileOptions: CompileOptions?): string
return nil :: any
end
--[=[
@within Luau
Generates a function from either bytecode or sourcecode
An error will be thrown if the sourcecode given isn't valid luau code.
### Example usage
```lua
local luau = require("@lune/luau")
local bytecode = luau.compile("print('Hello, World!')")
local callableFn = luau.load(bytecode, {
debugName = "'Hello, World'"
})
callableFn()
```
@param source Either luau bytecode or string source code
@param loadOptions The options passed to luau for loading the chunk
@return luau chunk
]=]
function luau.load(source: string, loadOptions: LoadOptions?): (...any) -> ...any
return nil :: any
end
return luau

View file

@ -1,321 +0,0 @@
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH"
type HttpQueryOrHeaderMap = { [string]: string | { string } }
export type HttpQueryMap = HttpQueryOrHeaderMap
export type HttpHeaderMap = HttpQueryOrHeaderMap
--[=[
@interface FetchParamsOptions
@within Net
Extra options for `FetchParams`.
This is a dictionary that may contain one or more of the following values:
* `decompress` - If the request body should be automatically decompressed when possible. Defaults to `true`
]=]
export type FetchParamsOptions = {
decompress: boolean?,
}
--[=[
@interface FetchParams
@within Net
Parameters for sending network requests with `net.request`.
This is a dictionary that may contain one or more of the following values:
* `url` - The URL to send a request to. This is always required
* `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Defaults to `"GET"`
* `body` - The request body
* `query` - A table of key-value pairs representing query parameters in the request path
* `headers` - A table of key-value pairs representing headers
* `options` - Extra options for things such as automatic decompression of response bodies
]=]
export type FetchParams = {
url: string,
method: HttpMethod?,
body: (string | buffer)?,
query: HttpQueryMap?,
headers: HttpHeaderMap?,
options: FetchParamsOptions?,
}
--[=[
@interface FetchResponse
@within Net
Response type for sending network requests with `net.request`.
This is a dictionary containing the following values:
* `ok` - If the status code is a canonical success status code, meaning within the range 200 -> 299
* `statusCode` - The status code returned for the request
* `statusMessage` - The canonical status message for the returned status code, such as `"Not Found"` for status code 404
* `headers` - A table of key-value pairs representing headers
* `body` - The request body, or an empty string if one was not given
]=]
export type FetchResponse = {
ok: boolean,
statusCode: number,
statusMessage: string,
headers: HttpHeaderMap,
body: string,
}
--[=[
@interface ServeRequest
@within Net
Data type for requests in `net.serve`.
This is a dictionary containing the following values:
* `path` - The path being requested, relative to the root. Will be `/` if not specified
* `query` - A table of key-value pairs representing query parameters in the request path
* `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Will always be uppercase
* `headers` - A table of key-value pairs representing headers
* `body` - The request body, or an empty string if one was not given
]=]
export type ServeRequest = {
path: string,
query: { [string]: string? },
method: HttpMethod,
headers: { [string]: string },
body: string,
}
--[=[
@interface ServeResponse
@within Net
Response type for requests in `net.serve`.
This is a dictionary that may contain one or more of the following values:
* `status` - The status code for the request, in the range `100` -> `599`
* `headers` - A table of key-value pairs representing headers
* `body` - The response body
]=]
export type ServeResponse = {
status: number?,
headers: { [string]: string }?,
body: (string | buffer)?,
}
type ServeHttpHandler = (request: ServeRequest) -> string | ServeResponse
type ServeWebSocketHandler = (socket: WebSocket) -> ()
--[=[
@interface ServeConfig
@within Net
Configuration for `net.serve`.
This may contain one of or more of the following values:
* `address` for setting the IP address to serve from. Defaults to the loopback interface (`http://localhost`).
* `handleRequest` for handling normal http requests, equivalent to just passing a function to `net.serve`
* `handleWebSocket` for handling web socket requests, which will receive a `WebSocket` object as its first and only parameter
When setting `address`, the `handleRequest` callback must also be defined.
```lua
net.serve(8080, {
address = "http://0.0.0.0",
handleRequest = function(request)
return {
status = 200,
body = "Echo:\n" .. request.body,
}
end
})
```
]=]
export type ServeConfig = {
address: string?,
handleRequest: ServeHttpHandler?,
handleWebSocket: ServeWebSocketHandler?,
}
--[=[
@interface ServeHandle
@within Net
A handle to a currently running web server, containing a single `stop` function to gracefully shut down the web server.
]=]
export type ServeHandle = {
stop: () -> (),
}
--[=[
@interface WebSocket
@within Net
A reference to a web socket connection.
The web socket may be in either an "open" or a "closed" state, changing its current behavior.
When open:
* Any function on the socket such as `send`, `next` or `close` can be called without erroring
* `next` can be called to yield until the next message is received or the socket becomes closed
When closed:
* `next` will no longer return any message(s) and instead instantly return nil
* `send` will throw an error stating that the socket has been closed
Once the websocket has been closed, `closeCode` will no longer be nil, and will be populated with a close
code according to the [WebSocket specification](https://www.iana.org/assignments/websocket/websocket.xhtml).
This will be an integer between 1000 and 4999, where 1000 is the canonical code for normal, error-free closure.
]=]
export type WebSocket = {
closeCode: number?,
close: (code: number?) -> (),
send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (),
next: () -> string?,
}
--[=[
@class Net
Built-in library for network access
### Example usage
```lua
local net = require("@lune/net")
-- Sending a web request
local response = net.request("https://www.google.com")
print(response.ok)
print(response.statusCode, response.statusMessage)
print(response.headers)
-- Using a JSON web API
local response = net.request({
url = "https://dummyjson.com/products/add",
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = net.jsonEncode({
title = "Cool Pencil",
})
})
local product = net.jsonDecode(response.body)
print(product.id, "-", product.title)
-- Starting up a webserver
net.serve(8080, function(request)
return {
status = 200,
body = "Echo:\n" .. request.body,
}
end)
```
]=]
local net = {}
--[=[
@within Net
Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received.
Only throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes.
@param config The URL or request config to use
@return A dictionary representing the response for the request
]=]
function net.request(config: string | FetchParams): FetchResponse
return nil :: any
end
--[=[
@within Net
@tag must_use
Connects to a web socket at the given URL.
Throws an error if the server at the given URL does not support
web sockets, or if a miscellaneous network or I/O error occurs.
@param url The URL to connect to
@return A web socket handle
]=]
function net.socket(url: string): WebSocket
return nil :: any
end
--[=[
@within Net
Creates an HTTP server that listens on the given `port`.
This will ***not*** block and will keep listening for requests on the given `port`
until the `stop` function on the returned `ServeHandle` has been called.
@param port The port to use for the server
@param handlerOrConfig The handler function or config to use for the server
]=]
function net.serve(port: number, handlerOrConfig: ServeHttpHandler | ServeConfig): ServeHandle
return nil :: any
end
--[=[
@within Net
@tag must_use
Encodes the given value as JSON.
@param value The value to encode as JSON
@param pretty If the encoded JSON string should include newlines and spaces. Defaults to false
@return The encoded JSON string
]=]
function net.jsonEncode(value: any, pretty: boolean?): string
return nil :: any
end
--[=[
@within Net
@tag must_use
Decodes the given JSON string into a lua value.
@param encoded The JSON string to decode
@return The decoded lua value
]=]
function net.jsonDecode(encoded: string): any
return nil :: any
end
--[=[
@within Net
@tag must_use
Encodes the given string using URL encoding.
@param s The string to encode
@param binary If the string should be treated as binary data and/or is not valid utf-8. Defaults to false
@return The encoded string
]=]
function net.urlEncode(s: string, binary: boolean?): string
return nil :: any
end
--[=[
@within Net
@tag must_use
Decodes the given string using URL decoding.
@param s The string to decode
@param binary If the string should be treated as binary data and/or is not valid utf-8. Defaults to false
@return The decoded string
]=]
function net.urlDecode(s: string, binary: boolean?): string
return nil :: any
end
return net

View file

@ -1,182 +0,0 @@
export type OS = "linux" | "macos" | "windows"
export type Arch = "x86_64" | "aarch64"
export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
export type SpawnOptionsStdio = {
stdout: SpawnOptionsStdioKind?,
stderr: SpawnOptionsStdioKind?,
stdin: string?,
}
--[=[
@interface SpawnOptions
@within Process
A dictionary of options for `process.spawn`, with the following available values:
* `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
* `stdin` - Optional standard input to pass to spawned child process
]=]
export type SpawnOptions = {
cwd: string?,
env: { [string]: string }?,
shell: (boolean | string)?,
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
}
--[=[
@interface SpawnResult
@within Process
Result type for child processes in `process.spawn`.
This is a dictionary containing the following values:
* `ok` - If the child process exited successfully or not, meaning the exit code was zero or not set
* `code` - The exit code set by the child process, or 0 if one was not set
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
]=]
export type SpawnResult = {
ok: boolean,
code: number,
stdout: string,
stderr: string,
}
--[=[
@class Process
Built-in functions for the current process & child processes
### Example usage
```lua
local process = require("@lune/process")
-- Getting the arguments passed to the Lune script
for index, arg in process.args do
print("Process argument #" .. tostring(index) .. ": " .. arg)
end
-- Getting the currently available environment variables
local PORT: string? = process.env.PORT
local HOME: string? = process.env.HOME
for name, value in process.env do
print("Environment variable " .. name .. " is set to " .. value)
end
-- Getting the current os and processor architecture
print("Running " .. process.os .. " on " .. process.arch .. "!")
-- Spawning a child process
local result = process.spawn("program", {
"cli argument",
"other cli argument"
})
if result.ok then
print(result.stdout)
else
print(result.stderr)
end
```
]=]
local process = {}
--[=[
@within Process
@prop os OS
@tag read_only
The current operating system being used.
Possible values:
* `"linux"`
* `"macos"`
* `"windows"`
]=]
process.os = (nil :: any) :: OS
--[=[
@within Process
@prop arch Arch
@tag read_only
The architecture of the processor currently being used.
Possible values:
* `"x86_64"`
* `"aarch64"`
]=]
process.arch = (nil :: any) :: Arch
--[=[
@within Process
@prop args { string }
@tag read_only
The arguments given when running the Lune script.
]=]
process.args = (nil :: any) :: { string }
--[=[
@within Process
@prop cwd string
@tag read_only
The current working directory in which the Lune script is running.
]=]
process.cwd = (nil :: any) :: string
--[=[
@within Process
@prop env { [string]: string? }
@tag read_write
Current environment variables for this process.
Setting a value on this table will set the corresponding environment variable.
]=]
process.env = (nil :: any) :: { [string]: string? }
--[=[
@within Process
Exits the currently running script as soon as possible with the given exit code.
Exit code 0 is treated as a successful exit, any other value is treated as an error.
Setting the exit code using this function will override any otherwise automatic exit code.
@param code The exit code to set
]=]
function process.exit(code: number?): never
return nil :: any
end
--[=[
@within Process
Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process.
The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
@param program The program to spawn as a child process
@param params Additional parameters to pass to the program
@param options A dictionary of options for the child process
@return A dictionary representing the result of the child process
]=]
function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult
return nil :: any
end
return process

View file

@ -1,218 +0,0 @@
--[=[
@class RegexMatch
A match from a regular expression.
Contains the following values:
- `start` -- The start index of the match in the original string.
- `finish` -- The end index of the match in the original string.
- `text` -- The text that was matched.
- `len` -- The length of the text that was matched.
]=]
local RegexMatch = {
start = 0,
finish = 0,
text = "",
len = 0,
}
type RegexMatch = typeof(RegexMatch)
--[=[
@class RegexCaptures
Captures from a regular expression.
]=]
local RegexCaptures = {}
--[=[
@within RegexCaptures
@tag Method
Returns the match at the given index, if one exists.
@param index -- The index of the match to get
@return RegexMatch -- The match, if one exists
]=]
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
return nil :: any
end
--[=[
@within RegexCaptures
@tag Method
Returns the match for the given named match group, if one exists.
@param group -- The name of the group to get
@return RegexMatch -- The match, if one exists
]=]
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
return nil :: any
end
--[=[
@within RegexCaptures
@tag Method
Formats the captures using the given format string.
### Example usage
```lua
local regex = require("@lune/regex")
local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")
local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
assert(caps ~= nil, "Example pattern should match example text")
local formatted = caps:format("year=$year, month=$month, day=$day")
print(formatted) -- "year=2010, month=03, day=14"
```
@param format -- The format string to use
@return string -- The formatted string
]=]
function RegexCaptures.format(self: RegexCaptures, format: string): string
return nil :: any
end
export type RegexCaptures = typeof(RegexCaptures)
local Regex = {}
--[=[
@within Regex
@tag Method
Check if the given text matches the regular expression.
This method may be slightly more efficient than calling `find`
if you only need to know if the text matches the pattern.
@param text -- The text to search
@return boolean -- Whether the text matches the pattern
]=]
function Regex.isMatch(self: Regex, text: string): boolean
return nil :: any
end
--[=[
@within Regex
@tag Method
Finds the first match in the given text.
Returns `nil` if no match was found.
@param text -- The text to search
@return RegexMatch? -- The match object
]=]
function Regex.find(self: Regex, text: string): RegexMatch?
return nil :: any
end
--[=[
@within Regex
@tag Method
Finds all matches in the given text as a `RegexCaptures` object.
Returns `nil` if no matches are found.
@param text -- The text to search
@return RegexCaptures? -- The captures object
]=]
function Regex.captures(self: Regex, text: string): RegexCaptures?
return nil :: any
end
--[=[
@within Regex
@tag Method
Splits the given text using the regular expression.
@param text -- The text to split
@return { string } -- The split text
]=]
function Regex.split(self: Regex, text: string): { string }
return nil :: any
end
--[=[
@within Regex
@tag Method
Replaces the first match in the given text with the given replacer string.
@param haystack -- The text to search
@param replacer -- The string to replace matches with
@return string -- The text with the first match replaced
]=]
function Regex.replace(self: Regex, haystack: string, replacer: string): string
return nil :: any
end
--[=[
@within Regex
@tag Method
Replaces all matches in the given text with the given replacer string.
@param haystack -- The text to search
@param replacer -- The string to replace matches with
@return string -- The text with all matches replaced
]=]
function Regex.replaceAll(self: Regex, haystack: string, replacer: string): string
return nil :: any
end
export type Regex = typeof(Regex)
--[=[
@class Regex
Built-in library for regular expressions
### Example usage
```lua
local Regex = require("@lune/regex")
local re = Regex.new("hello")
if re:isMatch("hello, world!") then
print("Matched!")
end
local caps = re:captures("hello, world! hello, again!")
print(#caps) -- 2
print(caps:get(1)) -- "hello"
print(caps:get(2)) -- "hello"
print(caps:get(3)) -- nil
```
]=]
local regex = {}
--[=[
@within Regex
@tag Constructor
Creates a new `Regex` from a given string pattern.
### Errors
This constructor throws an error if the given pattern is invalid.
@param pattern -- The string pattern to use
@return Regex -- The new Regex object
]=]
function regex.new(pattern: string): Regex
return nil :: any
end
return regex

View file

@ -1,507 +0,0 @@
export type DatabaseScriptability = "None" | "Custom" | "Read" | "ReadWrite" | "Write"
export type DatabasePropertyTag =
"Deprecated"
| "Hidden"
| "NotBrowsable"
| "NotReplicated"
| "NotScriptable"
| "ReadOnly"
| "WriteOnly"
export type DatabaseClassTag =
"Deprecated"
| "NotBrowsable"
| "NotCreatable"
| "NotReplicated"
| "PlayerReplicated"
| "Service"
| "Settings"
| "UserSettings"
export type DatabaseProperty = {
--[=[
The name of the property.
]=]
Name: string,
--[=[
The datatype of the property.
For normal datatypes this will be a string such as `string`, `Color3`, ...
For enums this will be a string formatted as `Enum.EnumName`.
]=]
Datatype: string,
--[=[
The scriptability of this property, meaning if it can be written / read at runtime.
All properties are writable and readable in Lune even if scriptability is not.
]=]
Scriptability: DatabaseScriptability,
--[=[
Tags describing the property.
These include information such as if the property can be replicated to players
at runtime, if the property should be hidden in Roblox Studio, and more.
]=]
Tags: { DatabasePropertyTag },
}
export type DatabaseClass = {
--[=[
The name of the class.
]=]
Name: string,
--[=[
The superclass (parent class) of this class.
May be nil if no parent class exists.
]=]
Superclass: string?,
--[=[
Known properties for this class.
]=]
Properties: { [string]: DatabaseProperty },
--[=[
Default values for properties of this class.
Note that these default properties use Lune's built-in datatype
userdatas, and that if there is a new datatype that Lune does
not yet know about, it may be missing from this table.
]=]
DefaultProperties: { [string]: any },
--[=[
Tags describing the class.
These include information such as if the class can be replicated
to players at runtime, and top-level class categories.
]=]
Tags: { DatabaseClassTag },
}
export type DatabaseEnum = {
--[=[
The name of this enum, for example `PartType` or `UserInputState`.
]=]
Name: string,
--[=[
Members of this enum.
Note that this is a direct map of name -> enum values,
and does not actually use the EnumItem datatype itself.
]=]
Items: { [string]: number },
}
export type Database = {
--[=[
The current version of the reflection database.
This will follow the format `x.y.z.w`, which most commonly looks something like `0.567.0.123456789`
]=]
Version: string,
--[=[
Retrieves a list of all currently known class names.
]=]
GetClassNames: (self: Database) -> { string },
--[=[
Retrieves a list of all currently known enum names.
]=]
GetEnumNames: (self: Database) -> { string },
--[=[
Gets a class with the exact given name, if one exists.
]=]
GetClass: (self: Database, name: string) -> DatabaseClass?,
--[=[
Gets an enum with the exact given name, if one exists.
]=]
GetEnum: (self: Database, name: string) -> DatabaseEnum?,
--[=[
Finds a class with the given name.
This will use case-insensitive matching and ignore leading and trailing whitespace.
]=]
FindClass: (self: Database, name: string) -> DatabaseClass?,
--[=[
Finds an enum with the given name.
This will use case-insensitive matching and ignore leading and trailing whitespace.
]=]
FindEnum: (self: Database, name: string) -> DatabaseEnum?,
}
type InstanceProperties = {
Parent: Instance?,
ClassName: string,
Name: string,
-- FIXME: This breaks intellisense, but we need some way to access
-- instance properties without casting the entire instance to any...
-- [string]: any,
}
type InstanceMetatable = {
Clone: (self: Instance) -> Instance,
Destroy: (self: Instance) -> (),
ClearAllChildren: (self: Instance) -> (),
GetChildren: (self: Instance) -> { Instance },
GetDebugId: (self: Instance) -> string,
GetDescendants: (self: Instance) -> { Instance },
GetFullName: (self: Instance) -> string,
FindFirstAncestor: (self: Instance, name: string) -> Instance?,
FindFirstAncestorOfClass: (self: Instance, className: string) -> Instance?,
FindFirstAncestorWhichIsA: (self: Instance, className: string) -> Instance?,
FindFirstChild: (self: Instance, name: string, recursive: boolean?) -> Instance?,
FindFirstChildOfClass: (self: Instance, className: string, recursive: boolean?) -> Instance?,
FindFirstChildWhichIsA: (self: Instance, className: string, recursive: boolean?) -> Instance?,
IsA: (self: Instance, className: string) -> boolean,
IsAncestorOf: (self: Instance, descendant: Instance) -> boolean,
IsDescendantOf: (self: Instance, ancestor: Instance) -> boolean,
GetAttribute: (self: Instance, name: string) -> any,
GetAttributes: (self: Instance) -> { [string]: any },
SetAttribute: (self: Instance, name: string, value: any) -> (),
GetTags: (self: Instance) -> { string },
HasTag: (self: Instance, name: string) -> boolean,
AddTag: (self: Instance, name: string) -> (),
RemoveTag: (self: Instance, name: string) -> (),
}
export type Instance = typeof(setmetatable(
(nil :: any) :: InstanceProperties,
(nil :: any) :: { __index: InstanceMetatable }
))
export type DataModelProperties = {}
export type DataModelMetatable = {
GetService: (self: DataModel, name: string) -> Instance,
FindService: (self: DataModel, name: string) -> Instance?,
}
export type DataModel =
Instance
& typeof(setmetatable(
(nil :: any) :: DataModelProperties,
(nil :: any) :: { __index: DataModelMetatable }
))
--[=[
@class Roblox
Built-in library for manipulating Roblox place & model files
### Example usage
```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox")
-- Reading a place file
local placeFile = fs.readFile("myPlaceFile.rbxl")
local game = roblox.deserializePlace(placeFile)
-- Manipulating and reading instances - just like in Roblox!
local workspace = game:GetService("Workspace")
for _, child in workspace:GetChildren() do
print("Found child " .. child.Name .. " of class " .. child.ClassName)
end
-- Writing a place file
local newPlaceFile = roblox.serializePlace(game)
fs.writeFile("myPlaceFile.rbxl", newPlaceFile)
```
]=]
local roblox = {}
--[=[
@within Roblox
@tag must_use
Deserializes a place into a DataModel instance.
This function accepts a string of contents, *not* a file path.
If reading a place file from a file path is desired, `fs.readFile`
can be used and the resulting string may be passed to this function.
### Example usage
```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox")
local placeFile = fs.readFile("filePath.rbxl")
local game = roblox.deserializePlace(placeFile)
```
@param contents The contents of the place to read
]=]
function roblox.deserializePlace(contents: string): DataModel
return nil :: any
end
--[=[
@within Roblox
@tag must_use
Deserializes a model into an array of instances.
This function accepts a string of contents, *not* a file path.
If reading a model file from a file path is desired, `fs.readFile`
can be used and the resulting string may be passed to this function.
### Example usage
```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox")
local modelFile = fs.readFile("filePath.rbxm")
local instances = roblox.deserializeModel(modelFile)
```
@param contents The contents of the model to read
]=]
function roblox.deserializeModel(contents: string): { Instance }
return nil :: any
end
--[=[
@within Roblox
@tag must_use
Serializes a place from a DataModel instance.
This string can then be written to a file, or sent over the network.
### Example usage
```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox")
local placeFile = roblox.serializePlace(game)
fs.writeFile("filePath.rbxl", placeFile)
```
@param dataModel The DataModel for the place to serialize
@param xml If the place should be serialized as xml or not. Defaults to `false`, meaning the place gets serialized using the binary format and not xml.
]=]
function roblox.serializePlace(dataModel: DataModel, xml: boolean?): string
return nil :: any
end
--[=[
@within Roblox
@tag must_use
Serializes one or more instances as a model.
This string can then be written to a file, or sent over the network.
### Example usage
```lua
local fs = require("@lune/fs")
local roblox = require("@lune/roblox")
local modelFile = roblox.serializeModel({ instance1, instance2, ... })
fs.writeFile("filePath.rbxm", modelFile)
```
@param instances The array of instances to serialize
@param xml If the model should be serialized as xml or not. Defaults to `false`, meaning the model gets serialized using the binary format and not xml.
]=]
function roblox.serializeModel(instances: { Instance }, xml: boolean?): string
return nil :: any
end
--[=[
@within Roblox
@tag must_use
Gets the current auth cookie, for usage with Roblox web APIs.
Note that this auth cookie is formatted for use as a "Cookie" header,
and that it contains restrictions so that it may only be used for
official Roblox endpoints. To get the raw cookie value without any
additional formatting, you can pass `true` as the first and only parameter.
### Example usage
```lua
local roblox = require("@lune/roblox")
local net = require("@lune/net")
local cookie = roblox.getAuthCookie()
assert(cookie ~= nil, "Failed to get roblox auth cookie")
local myPrivatePlaceId = 1234567890
local response = net.request({
url = "https://assetdelivery.roblox.com/v2/assetId/" .. tostring(myPrivatePlaceId),
headers = {
Cookie = cookie,
},
})
local responseTable = net.jsonDecode(response.body)
local responseLocation = responseTable.locations[1].location
print("Download link to place: " .. responseLocation)
```
@param raw If the cookie should be returned as a pure value or not. Defaults to false
]=]
function roblox.getAuthCookie(raw: boolean?): string?
return nil :: any
end
--[=[
@within Roblox
@tag must_use
Gets the bundled reflection database.
This database contains information about Roblox enums, classes, and their properties.
### Example usage
```lua
local roblox = require("@lune/roblox")
local db = roblox.getReflectionDatabase()
print("There are", #db:GetClassNames(), "classes in the reflection database")
print("All base instance properties:")
local class = db:GetClass("Instance")
for name, prop in class.Properties do
print(string.format(
"- %s with datatype %s and default value %s",
prop.Name,
prop.Datatype,
tostring(class.DefaultProperties[prop.Name])
))
end
```
]=]
function roblox.getReflectionDatabase(): Database
return nil :: any
end
--[=[
@within Roblox
Implements a property for all instances of the given `className`.
This takes into account class hierarchies, so implementing a property
for the `BasePart` class will also implement it for `Part` and others,
unless a more specific implementation is added to the `Part` class directly.
### Behavior
The given `getter` callback will be called each time the property is
indexed, with the instance as its one and only argument. The `setter`
callback, if given, will be called each time the property should be set,
with the instance as the first argument and the property value as second.
### Example usage
```lua
local roblox = require("@lune/roblox")
local part = roblox.Instance.new("Part")
local propertyValues = {}
roblox.implementProperty(
"BasePart",
"CoolProp",
function(instance)
if propertyValues[instance] == nil then
propertyValues[instance] = 0
end
propertyValues[instance] += 1
return propertyValues[instance]
end,
function(instance, value)
propertyValues[instance] = value
end
)
print(part.CoolProp) --> 1
print(part.CoolProp) --> 2
print(part.CoolProp) --> 3
part.CoolProp = 10
print(part.CoolProp) --> 11
print(part.CoolProp) --> 12
print(part.CoolProp) --> 13
```
@param className The class to implement the property for.
@param propertyName The name of the property to implement.
@param getter The function which will be called to get the property value when indexed.
@param setter The function which will be called to set the property value when indexed. Defaults to a function that will error with a message saying the property is read-only.
]=]
function roblox.implementProperty<T>(
className: string,
propertyName: string,
getter: (instance: Instance) -> T,
setter: ((instance: Instance, value: T) -> ())?
)
return nil :: any
end
--[=[
@within Roblox
Implements a method for all instances of the given `className`.
This takes into account class hierarchies, so implementing a method
for the `BasePart` class will also implement it for `Part` and others,
unless a more specific implementation is added to the `Part` class directly.
### Behavior
The given `callback` will be called every time the method is called,
and will receive the instance it was called on as its first argument.
The remaining arguments will be what the caller passed to the method, and
all values returned from the callback will then be returned to the caller.
### Example usage
```lua
local roblox = require("@lune/roblox")
local part = roblox.Instance.new("Part")
roblox.implementMethod("BasePart", "TestMethod", function(instance, ...)
print("Called TestMethod on instance", instance, "with", ...)
end)
part:TestMethod("Hello", "world!")
--> Called TestMethod on instance Part with Hello, world!
```
@param className The class to implement the method for.
@param methodName The name of the method to implement.
@param callback The function which will be called when the method is called.
]=]
function roblox.implementMethod(
className: string,
methodName: string,
callback: (instance: Instance, ...any) -> ...any
)
return nil :: any
end
-- TODO: Make typedefs for all of the datatypes as well...
roblox.Instance = (nil :: any) :: {
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),
}
return roblox

View file

@ -1,200 +0,0 @@
--[=[
@within Serde
@interface EncodeDecodeFormat
A serialization/deserialization format supported by the Serde library.
Currently supported formats:
| Name | Learn More |
|:-------|:---------------------|
| `json` | https://www.json.org |
| `yaml` | https://yaml.org |
| `toml` | https://toml.io |
]=]
export type EncodeDecodeFormat = "json" | "yaml" | "toml"
--[=[
@within Serde
@interface CompressDecompressFormat
A compression/decompression format supported by the Serde library.
Currently supported formats:
| Name | Learn More |
|:---------|:----------------------------------|
| `brotli` | https://github.com/google/brotli |
| `gzip` | https://www.gnu.org/software/gzip |
| `lz4` | https://github.com/lz4/lz4 |
| `zlib` | https://www.zlib.net |
]=]
export type CompressDecompressFormat = "brotli" | "gzip" | "lz4" | "zlib"
--[=[
@within Serde
@interface HashAlgorithm
A hash algorithm supported by the Serde library.
Currently supported algorithms:
| Name | Learn More |
|:-----------|:-------------------------------------|
| `md5` | https://en.wikipedia.org/wiki/MD5 |
| `sha1` | https://en.wikipedia.org/wiki/SHA-1 |
| `sha224` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha256` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha384` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha512` | https://en.wikipedia.org/wiki/SHA-2 |
| `sha3-224` | https://en.wikipedia.org/wiki/SHA-3 |
| `sha3-256` | https://en.wikipedia.org/wiki/SHA-3 |
| `sha3-384` | https://en.wikipedia.org/wiki/SHA-3 |
| `sha3-512` | https://en.wikipedia.org/wiki/SHA-3 |
| `blake3` | https://en.wikipedia.org/wiki/BLAKE3 |
]=]
export type HashAlgorithm =
"md5"
| "sha1"
| "sha224"
| "sha256"
| "sha384"
| "sha512"
| "sha3-224"
| "sha3-256"
| "sha3-384"
| "sha3-512"
| "blake3"
--[=[
@class Serde
Built-in library for:
- serialization & deserialization
- encoding & decoding
- compression
### Example usage
```lua
local fs = require("@lune/fs")
local serde = require("@lune/serde")
-- Parse different file formats into lua tables
local someJson = serde.decode("json", fs.readFile("myFile.json"))
local someToml = serde.decode("toml", fs.readFile("myFile.toml"))
local someYaml = serde.decode("yaml", fs.readFile("myFile.yaml"))
-- Write lua tables to files in different formats
fs.writeFile("myFile.json", serde.encode("json", someJson))
fs.writeFile("myFile.toml", serde.encode("toml", someToml))
fs.writeFile("myFile.yaml", serde.encode("yaml", someYaml))
```
]=]
local serde = {}
--[=[
@within Serde
@tag must_use
Encodes the given value using the given format.
See [`EncodeDecodeFormat`] for a list of supported formats.
@param format The format to use
@param value The value to encode
@param pretty If the encoded string should be human-readable, including things such as newlines and spaces. Only supported for json and toml formats, and defaults to false
@return The encoded string
]=]
function serde.encode(format: EncodeDecodeFormat, value: any, pretty: boolean?): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Decodes the given string using the given format into a lua value.
See [`EncodeDecodeFormat`] for a list of supported formats.
@param format The format to use
@param encoded The string to decode
@return The decoded lua value
]=]
function serde.decode(format: EncodeDecodeFormat, encoded: buffer | string): any
return nil :: any
end
--[=[
@within Serde
@tag must_use
Compresses the given string using the given format.
See [`CompressDecompressFormat`] for a list of supported formats.
@param format The format to use
@param s The string to compress
@param level The compression level to use, clamped to the format's limits. The best compression level is used by default
@return The compressed string
]=]
function serde.compress(format: CompressDecompressFormat, s: buffer | string, level: number?): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Decompresses the given string using the given format.
See [`CompressDecompressFormat`] for a list of supported formats.
@param format The format to use
@param s The string to decompress
@return The decompressed string
]=]
function serde.decompress(format: CompressDecompressFormat, s: buffer | string): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Hashes the given message using the given algorithm
and returns the hash as a hex string.
See [`HashAlgorithm`] for a list of supported algorithms.
@param algorithm The algorithm to use
@param message The message to hash
@return The hash as a hex string
]=]
function serde.hash(algorithm: HashAlgorithm, message: string | buffer): string
return nil :: any
end
--[=[
@within Serde
@tag must_use
Hashes the given message using HMAC with the given secret
and algorithm, returning the hash as a base64 string.
See [`HashAlgorithm`] for a list of supported algorithms.
@param algorithm The algorithm to use
@param message The message to hash
@return The hash as a base64 string
]=]
function serde.hmac(
algorithm: HashAlgorithm,
message: string | buffer,
secret: string | buffer
): string
return nil :: any
end
return serde

View file

@ -1,161 +0,0 @@
export type Color =
"reset"
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| "purple"
| "cyan"
| "white"
export type Style = "reset" | "bold" | "dim"
type PromptFn = (
(() -> string)
& ((kind: "text", message: string?, defaultOrOptions: string?) -> string)
& ((kind: "confirm", message: string, defaultOrOptions: boolean?) -> boolean)
& ((kind: "select", message: string?, defaultOrOptions: { string }) -> number?)
& ((kind: "multiselect", message: string?, defaultOrOptions: { string }) -> { number }?)
)
--[=[
@within Stdio
@function prompt
@tag must_use
Prompts for user input using the wanted kind of prompt:
* `"text"` - Prompts for a plain text string from the user
* `"confirm"` - Prompts the user to confirm with y / n (yes / no)
* `"select"` - Prompts the user to select *one* value from a list
* `"multiselect"` - Prompts the user to select *one or more* values from a list
* `nil` - Equivalent to `"text"` with no extra arguments
@param kind The kind of prompt to use
@param message The message to show the user
@param defaultOrOptions The default value for the prompt, or options to choose from for selection prompts
]=]
local prompt: PromptFn = function(kind: any, message: any, defaultOrOptions: any)
return nil :: any
end
--[=[
@class Stdio
Built-in standard input / output & utility functions
### Example usage
```lua
local stdio = require("@lune/stdio")
-- Prompting the user for basic input
local text: string = stdio.prompt("text", "Please write some text")
local confirmed: boolean = stdio.prompt("confirm", "Please confirm this action")
-- Writing directly to stdout or stderr, without the auto-formatting of print/warn/error
stdio.write("Hello, ")
stdio.write("World! ")
stdio.write("All on the same line")
stdio.ewrite("\nAnd some error text, too")
-- Reading the entire input from stdin
local input = stdio.readToEnd()
```
]=]
local stdio = {}
stdio.prompt = prompt
--[=[
@within Stdio
@tag must_use
Return an ANSI string that can be used to modify the persistent output color.
Pass `"reset"` to get a string that can reset the persistent output color.
### Example usage
```lua
stdio.write(stdio.color("red"))
print("This text will be red")
stdio.write(stdio.color("reset"))
print("This text will be normal")
```
@param color The color to use
@return A printable ANSI string
]=]
function stdio.color(color: Color): string
return nil :: any
end
--[=[
@within Stdio
@tag must_use
Return an ANSI string that can be used to modify the persistent output style.
Pass `"reset"` to get a string that can reset the persistent output style.
### Example usage
```lua
stdio.write(stdio.style("bold"))
print("This text will be bold")
stdio.write(stdio.style("reset"))
print("This text will be normal")
```
@param style The style to use
@return A printable ANSI string
]=]
function stdio.style(style: Style): string
return nil :: any
end
--[=[
@within Stdio
@tag must_use
Formats arguments into a human-readable string with syntax highlighting for tables.
@param ... The values to format
@return The formatted string
]=]
function stdio.format(...: any): string
return nil :: any
end
--[=[
@within Stdio
Writes a string directly to stdout, without any newline.
@param s The string to write to stdout
]=]
function stdio.write(s: string) end
--[=[
@within Stdio
Writes a string directly to stderr, without any newline.
@param s The string to write to stderr
]=]
function stdio.ewrite(s: string) end
--[=[
@within Stdio
@tag must_use
Reads the entire input from stdin.
@return The input from stdin
]=]
function stdio.readToEnd(): string
return nil :: any
end
return stdio

View file

@ -1,99 +0,0 @@
--[=[
@class Task
Built-in task scheduler & thread spawning
### Example usage
```lua
local task = require("@lune/task")
-- Waiting for a certain amount of time
task.wait(1)
print("Waited for one second")
-- Running a task after a given amount of time
task.delay(2, function()
print("Ran after two seconds")
end)
-- Spawning a new task that runs concurrently
task.spawn(function()
print("Running instantly")
task.wait(1)
print("One second passed inside the task")
end)
print("Running after task.spawn yields")
```
]=]
local task = {}
--[=[
@within Task
Stops a currently scheduled thread from resuming.
@param thread The thread to cancel
]=]
function task.cancel(thread: thread) end
--[=[
@within Task
Defers a thread or function to run at the end of the current task queue.
@param functionOrThread The function or thread to defer
@return The thread that will be deferred
]=]
function task.defer<T...>(functionOrThread: thread | (T...) -> ...any, ...: T...): thread
return nil :: any
end
--[=[
@within Task
Delays a thread or function to run after `duration` seconds.
@param functionOrThread The function or thread to delay
@return The thread that will be delayed
]=]
function task.delay<T...>(
duration: number,
functionOrThread: thread | (T...) -> ...any,
...: T...
): thread
return nil :: any
end
--[=[
@within Task
Instantly runs a thread or function.
If the spawned task yields, the thread that spawned the task
will resume, letting the spawned task run in the background.
@param functionOrThread The function or thread to spawn
@return The thread that was spawned
]=]
function task.spawn<T...>(functionOrThread: thread | (T...) -> ...any, ...: T...): thread
return nil :: any
end
--[=[
@within Task
Waits for *at least* the given amount of time.
The minimum wait time possible when using `task.wait` is limited by the underlying OS sleep implementation.
For most systems this means `task.wait` is accurate down to about 5 milliseconds or less.
@param duration The amount of time to wait
@return The exact amount of time waited
]=]
function task.wait(duration: number?): number
return nil :: any
end
return task

View file

@ -1,8 +0,0 @@
--!strict
local spawn = require("util/spawn")
spawn.start("lune run install-packages")
spawn.start("rojo sourcemap dev.project.json -o sourcemap.json")
spawn.start(
"luau-lsp analyze --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore Packages/**/*.lua --ignore Packages/**/*.luau --ignore Packages/*.lua --ignore Packages/*.luau lib/"
)

View file

@ -1,7 +1,4 @@
--!strict
local spawn = require("util/spawn")
local spawn = require("./util/spawn")
spawn.start("lune run install-packages")
spawn.start("rojo sourcemap dev.project.json -o sourcemap.json")
spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } })
spawn.start("rojo build default.project.json -o build.rbxm")
spawn.start("rojo build default.project.json -o hammer.rbxm")

View file

@ -1,8 +1,6 @@
--!strict
local spawn = require("util/spawn")
local spawn = require("./util/spawn")
spawn.start("lune run analyze")
spawn.start("stylua lib/")
spawn.start("selene lib/")
spawn.start("lune run download-jecs")
spawn.start("luau test/tests.luau")

View file

@ -1,57 +1,10 @@
--!strict
local process = require("@lune/process")
local stdio = require("@lune/stdio")
local task = require("@lune/task")
local spawn = require("util/spawn")
local watch = require("util/watch")
local spawn = require("./util/spawn")
local watch = require("./util/watch")
task.spawn(watch, "wally.toml", function()
spawn.spawn("lune run install-packages")
task.spawn(watch, "pesde.toml", function()
spawn.spawn("pesde install")
end, false)
spawn.start("lune run install-packages")
spawn.spawn("rojo sourcemap dev.project.json -o sourcemap.json --watch")
spawn.spawn("darklua process --config .darklua.json --watch lib/ dist/", { env = { ROBLOX_DEV = "true" } })
task.wait(2.5)
while true do
local start_commit = stdio.prompt("confirm", "Start commit? -- `y` to start a commit, `n` to exit the script")
if not start_commit then
process.exit(0)
break
end
local _, check_result = pcall(spawn.start, "lune run check")
if not check_result.ok then
warn("Check didn't go ok, aborting commit")
break
end
local commit_title = stdio.prompt("text", "Commit title -- leave blank to stop committing")
if not commit_title or commit_title == "" then
print("Stopping commit")
continue
end
local commit_messages = { `-m`, commit_title }
while true do
local commit_message = stdio.prompt("text", "Commit message -- added to the description, leave blank to finish")
if not commit_message or commit_message == "" then
break
end
table.insert(commit_messages, "-m")
table.insert(commit_messages, commit_message)
end
local confirm = stdio.prompt("confirm", "Confirm?")
if not confirm then
break
end
spawn.start("git add .")
process.spawn("git", { "commit", unpack(commit_messages) }, { stdio = "forward" })
spawn.start("git push")
end
spawn.start("pesde install")

View file

@ -1,21 +0,0 @@
--!strict
local fs = require("@lune/fs")
local spawn = require("util/spawn")
spawn.start("rojo sourcemap dev.project.json -o sourcemap.json")
spawn.start("lune run install-packages")
spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } })
for _, path in fs.readDir("dist") do
path = `dist/{path}`
if not fs.isFile(path) then
continue
end
print("found working file")
local file = fs.readFile(path)
local new_contents =
string.gsub(file, `require%("%.%./jecs"%)`, `require(script.Parent.Parent:FindFirstChild('jecs'))`)
fs.writeFile(path, new_contents)
end

View file

@ -1,69 +0,0 @@
--!strict
local fs = require("@lune/fs")
local net = require("@lune/net")
local process = require("@lune/process")
local serde = require("@lune/serde")
local spawn = require("util/spawn")
type wally_manifest = {
package: {
name: string,
version: string,
registry: string,
realm: string,
license: string?,
exclude: { string }?,
include: { string }?,
},
dependencies: {
[string]: string,
},
}
local github_token: string = process.args[1]
if not github_token then
local env_exists = fs.metadata(".env").exists
if not env_exists then
error("Usage: lune run download-jecs [GITHUB_PAT]\nAlternatively, put the PAT in an .env file under GITHUB_PAT")
end
local env = serde.decode("toml", fs.readFile(".env"))
local pat = env.GITHUB_PAT or error("Couldn't read GITHUB_PAT from .env")
github_token = pat
end
local manifest_contents = fs.readFile("wally.toml") or error("Couldn't read manifest.")
local manifest: wally_manifest = serde.decode("toml", manifest_contents) or error("Couldn't decode manifest.")
local jecs_version = string.match(manifest.dependencies.jecs, "%d.%d.%d") or error("Couldn't find jecs version.")
type gh_api_tag = {
ref: string,
node_id: string,
url: string,
object: {
sha: string,
type: string,
url: string,
},
}
local response = net.request({
url = `https://api.github.com/repos/ukendio/jecs/git/refs/tags/v{jecs_version}`,
method = "GET",
headers = {
Accept = "application/vnd.github+json",
Authorization = `Bearer {github_token}`,
["X-GitHub-Api-Version"] = "2022-11-28",
},
})
if not response.ok then
error(`Github api response not ok:\n{response.statusCode} @ {response.statusMessage}\n{response.body}`)
end
local gh_api_tag: gh_api_tag = serde.decode("json", response.body)
spawn.start(
`curl https://raw.githubusercontent.com/ukendio/jecs/{gh_api_tag.object.sha}/src/init.luau -o jecs_src.luau`
)

View file

@ -1,6 +0,0 @@
--!strict
local spawn = require("util/spawn")
spawn.start("wally install")
spawn.start("rojo sourcemap dev.project.json -o sourcemap.json")
spawn.start("wally-package-types --sourcemap sourcemap.json Packages/")

View file

@ -9,15 +9,15 @@ local task = require("@lune/task")
--- @param cmd string
--- @param options process.SpawnOptions?
--- @return process.SpawnResult
local function start_process(cmd: string, options: process.SpawnOptions?): process.SpawnResult
local function start_process(cmd: string, options: process.ExecOptions?): process.ExecResult
local arguments = string.split(cmd, " ")
local command = arguments[1]
table.remove(arguments, 1)
local opts: process.SpawnOptions = options ~= nil and options or {}
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward"
local opts: process.ExecOptions = options ~= nil and options or {}
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward" :: any
return process.spawn(command, arguments, opts)
return process.exec(command, arguments, opts)
end
--- `task.spawn` a process with the given command and options
@ -27,7 +27,7 @@ end
--- @param cmd string
--- @param options process.SpawnOptions?
--- @return process.SpawnResult
local function spawn_process(cmd: string, options: process.SpawnOptions?)
local function spawn_process(cmd: string, options: process.ExecOptions?)
task.spawn(start_process, cmd, options)
end

View file

@ -1,44 +1,47 @@
// 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
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"lsp": {
"luau-lsp": {
"settings": {
"luau-lsp": {
"diagnostics": {
"workspace": false
},
"completion": {
"imports": {
"enabled": true,
"suggestServices": true,
"suggestRequires": false
"suggestRequires": true,
"stringRequires": {
"enabled": true
}
}
},
"sourcemap": {
"rojoProjectFile": "dev.project.json"
},
"require": {
"mode": "relativeToFile"
}
},
"ext": {
"roblox": {
"enabled": false
"enabled": false,
"security_level": "roblox_script"
},
"fflags": {
"override": {
"LuauTinyControlFlowAnalysis": "true"
},
"enable_new_solver": true,
"sync": true,
"enable_by_default": false
},
"binary": {
"ignore_system_version": false
}
}
}
}
},
"languages": {
"TypeScript": {
"tab_size": 4
}
"file_types": {
"Luau": ["lua"]
}
}

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Mark-Marks
Copyright (c) 2025 marked
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

117
README.md
View file

@ -1,18 +1,113 @@
# sapphire-utils
[![CI](https://img.shields.io/github/actions/workflow/status/mark-marks/jecs-utils/ci.yml?style=for-the-badge&label=CI)](https://github.com/mark-marks/jecs-utils/actions/workflows/ci.yml)
[![Wally](https://img.shields.io/github/v/tag/mark-marks/jecs-utils?&style=for-the-badge)](https://wally.run/package/mark-marks/jecs-utils)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://github.com/Mark-Marks/jecs-utils/blob/main/LICENSE)
<p align="center">
<img src="assets/hammer-logo.png">
</p>
[![CI](https://git.devmarked.win/marked/hammer/badges/workflows/ci.yml/badge.svg?style=for-the-badge&label=CI)](https://git.devmarked.win/marked/hammer/actions?workflow=ci.yml)
[![CD](https://git.devmarked.win/marked/hammer/badges/workflows/release.yml/badge.svg?style=for-the-badge&label=RELEASE)](https://git.devmarked.win/marked/hammer/actions?workflow=release.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://git.devmarked.win/marked/hammer/src/branch/main/LICENSE)
<a href="https://wally.run/package/mark-marks/hammer"><img alt="Wally" src="https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fgit.devmarked.win%2Fmarked%2Fhammer%2Fraw%2Fbranch%2Fmain%2Fwally.toml&query=package.version&prefix=mark-marks%2Fhammer%40&style=for-the-badge&label=Wally&color=ad4646&logo=data:image/svg%2bxml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+V2FsbHk8L3RpdGxlPjxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0yMC4zMjUgMTguMTkxYy0xLjkxNSAyLjU5OS01LjEyNyA0LjI1NC04LjM1OCA0LjE4MS0uMjk2LS41MjgtLjc2My0xLjY3My4zNDgtMS4yOTcgMi4zNTUtLjA3NiA0Ljc3LTEuMDE0IDYuMzc1LTIuNzYxLjI5OS0uODUzLjgyLS45ODcgMS4zOC0uMzAxbC4xMjcuMDkuMTI4LjA4OHpNMTIuNzg1IDYuMmMtLjg5Mi4yNjQtLjEwNCAyLjY2LjQ1OSAxLjI3Mi0uMDc1LS40MDcuMjItMS4yODgtLjQ1OC0xLjI3MnptLS41OS0uMjQyYy0uNjY0LTEuMzM1IDEuOTY2LS4zNTMgMS44ODItLjIyOC0uMzI2LS44NTYtMi4zMDItMS4yNC0yLjI2My0uMTA4bC4xNzMuMTk3ek0xMS41NCAxOS4zOGMtLjI4LTEuMzY0IDEuOTY1LS45NTggMS45My0xLjgwMS0uOTkyLS4xNi0yLjM4Mi0uODMyLTEuMzQtMS45NjMgMS4wMjctMS4wMjIgMi41MzMtMS45NTYgMi40OTItMy42NDktLjI4NS42MTItLjkyIDEuOTMtMS44MzUgMi4zODctMS41NzMgMS4wOC00LjA5IDEuMTc5LTUuMjYtLjU1LS4zNDktLjQ2My0uNjg3LTEuNDkxLS40NC0uMzQyLjQ2Ni42NjguNiAxLjcwMi0uNTYxIDEuNDUzLTEuMjQ1LS40NDEtLjM2Mi0xLjc2NC0uNC0yLjY0Ni0uNi0xLjE0NCAxLjM3Ni0uNjA4IDEuNjIzLTEuNjk0QzguNjQgOS40MyA2LjcyIDguODMgNS44NDggOC45MWMtLjk5Ni4xNjUuODUxLS40OTUgMS4xOC0uNzkuNzczLS40NTMgMS41MDYtLjk5NiAyLjA5LTEuNjgyLS41NjIuNDgyLS43NjEuNTE2LS43NDktLjI4LTEuMTUyLS41Ny0uMTM3IDEuNjkzLTEuMzk3IDEuNjY4LS45MTIuNjA1LjYxOS0xLjE0NC4yMzItMS43ODctLjIxOS0xLjIzNCAxLjUtMS4zMjIgMS40My0uMjMuNzYyLS42MjQtLjYxNi0xLjAyMy0uNjE2LTEuMTczIDEuMzQ3LTEuMzA3IDMuNDEzLTEuMzk1IDUuMTItLjg3My45MTYuMjUgMS43MDQuODYyIDIuMDA2IDEuNzg2Ljg5NCAyLjA2NC40NzMgNC4zNTEuMjc4IDYuNTA0LS4xOCAxLjExNi40OTMgMi4wNzcgMS4zODEgMi40NjYuNDI2LjkxNyAxLjkxIDEuNzUyLjU3NSAyLjYwOC0xLjUzOSAxLjQ4OC0zLjY2MyAyLjQ3Ny01LjgzOCAyLjI1MnptOS4xMjMtMS42NjVjLTEuMjctLjQ3MS0xLjc3My0xLjc0Mi0yLjg4NC0yLjM2NS0uNTMzLS42MzgtLjk2LTEuMTU0LS4yOS0xLjc4My4yOTktMS4zNjggMS43OC0xLjg1MiAyLjQ1NC0yLjk4Ljc4Ny0uOTY4LjcwNC0yLjQzMS0uMjAyLTMuMjkxLS43OTctLjg2LTIuMDc2LTEuMjA2LTIuNTI3LTIuMzg1LTEuMjMtMS4wMi0zLjAyMS0xLjA1NS00LjQ5OS0xLjY3NS0xLjMyOC0uMTk0LTIuOTA1LS4yNjEtNC4wMjEuNjA2LTEuNDkyLjAzLTEuODA3IDEuNzc3LTIuNTk0IDIuNzI2LS43My42NDktMS42NTMgMS4yNjYtMS4xNTMgMi4zMzQtMS4wNDguNzE3LjE3OCAyLjAzNi42OTIgMi43NTQuMzA3IDEuMjAyLS45OTQgMy4xNzYuOTY4IDMuNTM4Ljc4NC4wMjYgMS4xNzMtLjg2OCAxLjc5Ni0uMDQzIDEuMzc1LjIyNSAxLjA5IDEuODk4IDEuMDE4IDIuOTM2LjA4Mi45MDItMS4wMiAxLjU2NS0uMzI5IDIuNS0uMTQuODc4LS4zMDMgMS42Ni0xLjI3Ni45MjMtMy45OTktMS43MTgtNi42NDktNi4xMy02LjE2Ny0xMC40NzMuMzM0LTQuMTIyIDMuMzc3LTcuODM0IDcuMzQ1LTguOTg4IDQuMDgtMS4zMSA4Ljg0Ny4yODggMTEuMzUzIDMuNzU1IDIuNTg0IDMuNDAxIDIuNzMxIDguMzguMzE2IDExLjkxWk0xMS43NjguMDAzQzYuODQ4LjAzOSAyLjE4NSAzLjQ0NS42NTIgOC4xMmMtMS40OTUgNC4xOC0uMzU4IDkuMTEzIDIuNzc2IDEyLjI0OSAzLjI1NiAzLjQ0IDguNjMzIDQuNTY5IDEzLjAxIDIuNzc0IDQuNjM2LTEuNzg5IDcuODMtNi42OTIgNy41NDItMTEuNjYtLjE1NS00LjY2My0zLjMtOS4wNC03LjY3MS0xMC42NzJhMTEuODcyIDExLjg3MiAwIDAgMC00LjU0LS44MVoiIHN0eWxlPSJmaWxsOiNGRkY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiLz48L3N2Zz4=" /></a>
<a href="https://pesde.dev/packages/marked/hammer"><img alt="Pesde" src="https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fgit.devmarked.win%2Fmarked%2Fhammer%2Fraw%2Fbranch%2Fmain%2Fpesde.toml&query=version&prefix=marked%2Fhammer%40&style=for-the-badge&label=pesde&color=F19D1E&logo=data:image/svg%2bxml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OS42MDI1IDBMOTIuOTAzOCAyNVY3NUw0OS42MDI1IDEwMEw2LjMwMTI3IDc1VjI1TDQ5LjYwMjUgMFpNMTQuMzAxMyAyOS42MTg4TDQ5LjYwMjUgOS4yMzc2TDg0LjkwMzggMjkuNjE4OFY3MC4zODEyTDQ5LjYwMjUgOTAuNzYyNEwzMy42MTQ4IDgxLjUzMTlWNjcuMzg0OEMzNC41MTY3IDY4LjUwNzEgMzUuNjM4OCA2OS40MjE1IDM2Ljk4MSA3MC4xMjc5QzM4Ljk3MDEgNzEuMTQ4IDQxLjAzNTcgNzEuNjU4IDQzLjE3NzkgNzEuNjU4QzQ2LjQ0MiA3MS42NTggNDkuMTQ1MiA3MC44OTI5IDUxLjI4NzMgNjkuMzYyOUM1My40ODA1IDY3Ljc4MTggNTUuMTEyNiA2NS43NjcyIDU2LjE4MzYgNjMuMzE5QzU3LjA5MTUgNjEuMzM4MiA1Ny42MzIgNTkuMjc0IDU3LjgwNTQgNTcuMTI2M0M1OS44NzIzIDU3Ljc0NTcgNjIuMjE1NyA1OC4wNTU0IDY0LjgzNTYgNTguMDU1NEM2Ny42OTE4IDU4LjA1NTQgNzAuMzY5NSA1Ny42NDczIDcyLjg2ODYgNTYuODMxM0M3NS4zNjc4IDU1Ljk2NDIgNzcuNDA3OSA1NC44MTY3IDc4Ljk4OSA1My4zODg2TDc1Ljc3NTggNDcuODAzOEM3NC41NTE3IDQ4LjkyNTggNzIuOTk2MSA0OS44NDM5IDcxLjEwOSA1MC41NTc5QzY5LjIyMTkgNTEuMjIxIDY3LjIwNzMgNTEuNTUyNSA2NS4wNjUyIDUxLjU1MjVDNjEuMzkyOSA1MS41NTI1IDU4LjY2NDMgNTAuNjg1NCA1Ni44NzkyIDQ4Ljk1MTNDNTYuNzE5NSA0OC43OTYyIDU2LjU2NyA0OC42MzY1IDU2LjQyMTcgNDguNDcyQzU1LjYxMDIgNDcuNTUzOSA1NS4wMjExIDQ2LjQ4OTYgNTQuNjU0NiA0NS4yNzkxTDU0LjY0NDMgNDUuMjQ1Mkw1NC42NjkgNDUuMjc5MUg3OS4yMTg1VjQxLjk4OTRDNzkuMjE4NSAzOS4wMzEzIDc4LjU1NTUgMzYuMzUzNiA3Ny4yMjk0IDMzLjk1NjVDNzUuOTU0MyAzMS41NTkzIDc0LjA5MjcgMjkuNjQ2NyA3MS42NDQ1IDI4LjIxODZDNjkuMjQ3NCAyNi43Mzk1IDY2LjM2NTcgMjYgNjIuOTk5NSAyNkM1OS42ODQzIDI2IDU2LjgwMjcgMjYuNzM5NSA1NC4zNTQ1IDI4LjIxODZDNTEuOTA2NCAyOS42NDY3IDUwLjAxOTMgMzEuNTU5MyA0OC42OTMyIDMzLjk1NjVDNDcuNjc0MyAzNS43OTgzIDQ3LjA0NjkgMzcuODA1NyA0Ni44MTA4IDM5Ljk3ODhDNDUuNjg4OCAzOS43MjggNDQuNDc3OCAzOS42MDI2IDQzLjE3NzkgMzkuNjAyNkM0MS4wMzU3IDM5LjYwMjYgMzguOTcwMSA0MC4xMTI3IDM2Ljk4MSA0MS4xMzI3QzM1LjMxNjIgNDEuOTY1MSAzMy45OTAyIDQzLjE1NDkgMzMuMDAyOCA0NC43MDIzVjQwLjM2NzdIMjAuNjg1NVY0Ni4yNTg1SDI1LjgxMTNWNzcuMDI2NkwxNC4zMDEzIDcwLjM4MTJWMjkuNjE4OFpNNTUuMTk2MSAzNi4wOTg2QzU0LjY1MjggMzcuMTAxNSA1NC4zMzIxIDM4LjEyMTYgNTQuMjM0IDM5LjE1ODhINzEuNzk3NkM3MS43OTc2IDM4LjAzNjcgNzEuNDQwNSAzNi45NDAxIDcwLjcyNjUgMzUuODY5MUM3MC4wNjM0IDM0Ljc0NyA2OS4wNjg5IDMzLjgwMzUgNjcuNzQyOCAzMy4wMzg0QzY2LjQ2NzcgMzIuMjczNCA2NC44ODY3IDMxLjg5MDggNjIuOTk5NSAzMS44OTA4QzYxLjExMjQgMzEuODkwOCA1OS41MDU4IDMyLjI5ODkgNTguMTc5OCAzMy4xMTQ5QzU2LjkwNDcgMzMuODggNTUuOTEwMSAzNC44NzQ1IDU1LjE5NjEgMzYuMDk4NlpNNDkuNjQ1MSA1MS41NjkyQzQ5LjMwNzYgNTAuNjY0MSA0OC44MzgxIDQ5Ljg3MSA0OC4yMzY3IDQ5LjE4OThDNDguMDg4NSA0OS4wMjE5IDQ3LjkzMjMgNDguODYwOSA0Ny43NjgxIDQ4LjcwNjdDNDYuMDg1IDQ3LjA3NDYgNDQuMDQ0OSA0Ni4yNTg1IDQxLjY0NzggNDYuMjU4NUM0MC4xMTc3IDQ2LjI1ODUgMzguNjEzMSA0Ni41NjQ1IDM3LjEzNCA0Ny4xNzY2QzM1Ljg1OTQgNDcuNjc3MyAzNC42ODYzIDQ4LjU0MzggMzMuNjE0OCA0OS43NzU5VjYxLjQ3QzM0LjY4NjMgNjIuNjY2NCAzNS44NTk0IDYzLjUzNzggMzcuMTM0IDY0LjA4NEMzOC42MTMxIDY0LjY5NjEgNDAuMTE3NyA2NS4wMDIxIDQxLjY0NzggNjUuMDAyMUM0NC4wNDQ5IDY1LjAwMjEgNDYuMDg1IDY0LjE4NjEgNDcuNzY4MSA2Mi41NTRDNDkuNDUxMiA2MC45MjE5IDUwLjI5MjggNTguNjAxMiA1MC4yOTI4IDU1LjU5MjFDNTAuMjkyOCA1NC4wNjc5IDUwLjA3NjkgNTIuNzI3IDQ5LjY0NTEgNTEuNTY5MloiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KPC9zdmc+" /></a>
A set of utilities for [Jecs](https://github.com/ukendio/jecs)
<br/>
</div>
## Features
## Installation
- [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 iterator invalidation
- [spawner](/lib/spawner.luau) - Spawn entities with required components
Hammer is available on pesde @ `marked/hammer` and Wally @ `mark-marks/hammer`.
## Usage
All utilities that require a Jecs world to function are exposed via a constructor pattern.\
For instance, to build a `ref`:
```luau
local ref = hammer.ref(world)
```
This is the easiest solution for passing a world that doesn't sacrifice readability internally and externally or bind the developer to a Jecs version that hammer is currently using.
### collect
A [collect](/lib/utilities/collect.luau) collects all arguments fired through the given signal, and exposes an iterator for them.\
Its purpose is to interface with signals in ECS code, which ideally should run every frame in a loop.
For instance, take Roblox's RemoteEvents:
```luau
local pings = hammer.collect(events.ping)
local function system()
for _, player, ping in pings do
events.ping:FireClient(player, "pong!")
end
end
```
### command_buffer
A [command_buffer](/lib/utilities/command_buffer.luau) lets you buffer world commands in order to prevent iterator invalidation.\
Iterator invalidation refers to an iterator (e.g. `world:query(Component)`) becoming unusable due to changes in the underlying data.
To prevent this, command buffers can be used to delay world operations to the end of the current frame:
```luau
local command_buffer = hammer.command_buffer(world)
while true do
step_systems()
command_buffer.flush()
end
-- Inside a system:
command_buffer.add(entity, component) -- This runs after all of the systems run; no data changes while things are running
```
### ref
A [ref](/lib/utilities/ref.luau) allows for storing and getting entities via some form of reference.\
This is particularly useful for situations where you reconcile entities into your world from a foreign place, e.g. from across a networking boundary.
```luau
local ref = hammer.ref(world)
for id in net.new_entities.iter() do
local entity = ref(`foreign-{id}`) -- A new entity that can be tracked via a foreign id
end
```
Refs by default create a new entity if the given value doesn't reference any stored one. In case you want to see if a reference exists, you can find one:
```luau
local entity[: Entity?] = ref.find(`my-key`)
```
Refs can also be deleted. All functions used to a fetch a reference also return a cleanup function:
```luau
local entity, destroy_reference = ref(`my-key`)
destroy_reference() -- `entity` still persists in the world, but `my-key` doesn't refer to it anymore.
```
Refs are automatically cached by world. `ref(world)` will have the same underlying references as `ref(world)`.\
In case you need an unique reference store, you can omit the cache via `ref(world, true)`.
### tracker
A [tracker](/lib/utilities/tracker) keeps a history of all components passed to it, and how to get to their current state in the least amount of commands.\
They're great for replicating world state across a networking barrier, as you're able to easily get diffed snapshots and apply them.
```luau
local tracker = hammer.tracker(world, ComponentA, ComponentB)
world:set(entity_a, ComponentA, 50)
world:add(entity_b, ComponentB)
-- Says how to give `entity_a` `ComponentA` with the value of `50` and give `entity_b` `ComponentB`.
-- `state()` always tracks from when the tracker was first created.
local state = tracker.state()
-- Same as the above, but now this sets the origin for the next taken snapshot!
local snapshot = tracker.snapshot()
world:remove(entity_b, ComponentB)
-- This now only says to remove `ComponentB` from `entity_b`.
local snapshot_b = tracker.snapshot()
```
Trackers simplify the state internally. Removals remove all prior commands pertaining to the entity and component pair, adds remove all prior removals, etc.
Trackers are optimized under the hood with lookup tables for arrays, to allow for a constant time operation to check for whether it has a member or not. It can lead to worse memory usage, but makes it faster overall.

BIN
assets/hammer-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -1,6 +1,6 @@
{
"name": "jecs-utils",
"name": "hammer",
"tree": {
"$path": "dist"
"$path": "lib"
}
}

View file

@ -1,19 +0,0 @@
{
"name": "dev",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Packages": {
"$className": "Folder",
"$path": "Packages",
"jecs": {
"$path": "jecs.luau"
},
"jecs_utils": {
"$path": "lib"
}
}
}
}
}

View file

@ -6,8 +6,8 @@
-- this is here to mitigate that
local jecs = require("./luau_packages/jecs")
export type Archetype = jecs.Archetype
export type Id<T = nil> = jecs.Id<T>
export type Id<T = unknown> = jecs.Id<T>
export type Pair<First, Second> = jecs.Pair<First, Second>
export type Entity<T = nil> = jecs.Entity<T>
export type Entity<T = unknown> = jecs.Entity<T>
export type World = jecs.World
return jecs

View file

@ -1,135 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
local _world = require("./world")
local WORLD = _world.get
-- luau-lsp literally dies if you use the actual world type
type jecs_world = any
--- `map<component_id, array<entity_id>>`
local add_commands: { [jecs_world]: { [id]: { entity } } } = {}
--- `map<component_id, array<entity_id, component_value>>`
local set_commands: { [jecs_world]: { [id]: { [entity]: any } } } = {}
--- `map<component_id, array<entity_id>>`
local remove_commands: { [jecs_world]: { [id]: { entity } } } = {}
--- `array<entity_id>`
local delete_commands: { [jecs_world]: { entity } } = {}
_world.on_set(function(world)
add_commands[world] = {}
set_commands[world] = {}
remove_commands[world] = {}
delete_commands[world] = {}
end)
export 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()
for world, entities in delete_commands do
for _, entity in entities do
world:delete(entity)
end
end
for world, commands in add_commands do
for component, entities in commands do
for _, entity in entities do
if delete_commands[world][entity] then
continue
end
world:add(entity, component)
end
end
table.clear(add_commands[world])
end
for world, commands in set_commands do
for component, entities in commands do
for entity, value in entities do
if delete_commands[world][entity] then
continue
end
world:set(entity, component, value)
end
end
table.clear(set_commands[world])
end
for world, commands in remove_commands do
for component, entities in commands do
for _, entity in entities do
if delete_commands[world][entity] then
continue
end
world:remove(entity, component)
end
end
table.clear(remove_commands[world])
end
for world in delete_commands do
table.clear(delete_commands[world])
end
end
local function add(entity: entity, component: id)
local world = WORLD()
if not add_commands[world][component] then
add_commands[world][component] = {}
end
table.insert(add_commands[world][component], entity)
end
local function set<T>(entity: entity, component: id<T>, data: T)
local world = WORLD()
if not set_commands[world][component] then
set_commands[world][component] = {}
end
set_commands[world][component][entity] = data
end
local function remove(entity: entity, component: id)
local world = WORLD()
if not remove_commands[world][component] then
remove_commands[world][component] = {}
end
table.insert(remove_commands[world][component], entity)
end
local function delete(entity: entity)
local world = WORLD()
table.insert(delete_commands[world], entity)
end
local command_buffer: command_buffer = {
flush = flush,
add = add,
set = set,
remove = remove,
delete = delete,
}
return command_buffer

View file

@ -1,78 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
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, world: jecs.World }, {} :: interface))
local handle = {} :: interface
handle.__index = handle
function handle.new(entity: entity)
local self = {
entity = entity,
world = world(),
}
return setmetatable(self, handle)
end
function handle:has(...: id): boolean
return self.world:has(self.entity, ...)
end
handle.get = function(self: handle, a: id, b: id?, c: id?, d: id?)
return self.world:get(self.entity, a, b :: any, c :: any, d :: any)
end :: any
function handle:add<T>(id: id<T>): handle
self.world:add(self.entity, id)
return self
end
function handle:set<T>(id: id<T>, value: T): handle
self.world:set(self.entity, id, value)
return self
end
function handle:remove(id: id): handle
self.world:remove(self.entity, id)
return self
end
function handle:delete()
self.world:delete(self.entity)
end
function handle:id(): entity
return self.entity
end
return handle.new

View file

@ -1,42 +1,22 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
local collect = require("@self/utilities/collect")
export type SignalLike<T...> = collect.SignalLike<any, T...>
export type VerboseSignalLike<D, T...> = collect.SignalLike<D, T...>
local WORLD = require("./world")
local ref = require("@self/utilities/ref")
export type Ref = ref.Identity
local collect = require("./collect")
export type collect_signal_like<T...> = collect.signal_like<any, T...>
export type collect_verbose_signal_like<D, T...> = collect.signal_like<D, T...>
local tracker = require("@self/utilities/tracker")
export type Tracker = tracker.Identity
export type TrackerCommands = tracker.Commands
local command_buffer = require("./command_buffer")
export type command_buffer = command_buffer.command_buffer
local handle = require("./handle")
export type handle = handle.handle
local ref = require("./ref")
local replicator = require("./replicator")
export type replicator = replicator.replicator
export type changes = replicator.changes
local spawner = require("./spawner")
export type spawner<T...> = spawner.spawner<T...>
--- 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
local command_buffer = require("@self/utilities/command_buffer")
export type CommandBuffer = command_buffer.Identity
return {
initialize = initialize,
collect = collect,
handle = handle,
replicator = replicator,
ref = ref,
tracker = tracker,
command_buffer = command_buffer,
spawner = spawner,
}

View file

@ -1,67 +0,0 @@
--!strict
--!optimize 2
local handle = require("./handle")
local jecs = require("../jecs")
local WORLD = require("./world").get
local refs: { [jecs.World]: { [any]: jecs.Entity<any> } } = {}
local function serve_clearer(key: any, world: jecs.World): () -> ()
return function()
refs[world][key] = nil
end
end
--- 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, () -> ()?)
local world = WORLD()
if not key then
return handle(world:entity())
end
if not refs[world] then
refs[world] = {}
end
local entity = refs[world][key]
if not entity then
entity = world:entity()
refs[world][key] = entity
end
return handle(entity), serve_clearer(key, world)
end
-- For the `__call`` metamethod
local function __call(_, key: any): (handle.handle, () -> ()?)
return ref(key)
end
local function search(key: any): (handle.handle?, () -> ()?)
local world = WORLD()
if not key then
return nil
end
local entity = refs[world][key]
if not entity then
return nil
end
return handle(entity), serve_clearer(key, world)
end
local metatable = {
__call = __call,
__index = {
search = search,
set_ref = ref,
},
}
local REF = setmetatable({}, metatable) :: typeof(ref) & typeof(metatable.__index)
return REF

View file

@ -1,247 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../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 world = WORLD()
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

View file

@ -1,49 +0,0 @@
--!strict
local spawner_type = require("./spawner_type")
local WORLD = require("./world").get
local handle = require("./handle")
export type spawner<T...> = spawner_type.spawner<T...>
--- Creates an entity spawner.
--- ```luau
--- local spawner = jecs_utils.spawner(components.part, components.velocity, components.position)
--- for _ = 1, 1000 do
--- spawner.spawn(part_template:Clone(), Vector3.zero, Vector3.zero)
--- end
--- ```
--- @param ... T... -- Components to use.
--- @return spawner<T...>
local function spawner(...)
local components = { ... }
local world = WORLD()
local function spawn(...)
local passed = { ... }
local entity = world:entity()
for idx, component in components do
world:set(entity, component, passed[idx])
end
return entity
end
local function spawn_with_handle(...)
local passed = { ... }
local entity = handle(world:entity())
for idx, component in components do
entity:set(component, passed[idx])
end
return entity
end
return {
spawn = spawn,
spawn_with_handle = spawn_with_handle,
}
end
return (spawner :: any) :: spawner_type.create_spawner

View file

@ -1,391 +0,0 @@
--!strict
local jecs = require("../jecs")
type entity<T = nil> = jecs.Entity<T>
type id<T = nil> = jecs.Id<T>
local handle = require("./handle")
export type spawner<T...> = {
--- Creates an entity with the given components.
--- @param ... T...
--- @return entity
spawn: (T...) -> entity,
--- Creates an entity with the given components and returns a handle to it.
--- @param ... T...
--- @return handle
spawn_with_handle: (T...) -> handle.handle,
}
-- Very beautiful type incoming!
-- Sadly this has to be done, components are of different types than their values (`entity<T>` vs `T`)
export type create_spawner =
(<A>(id<A>) -> spawner<A>)
& (<A, B>(id<A>, id<B>) -> spawner<A, B>)
& (<A, B, C>(id<A>, id<B>, id<C>) -> spawner<A, B, C>)
& (<A, B, C, D>(id<A>, id<B>, id<C>, id<D>) -> spawner<A, B, C, D>)
& (<A, B, C, D, E>(id<A>, id<B>, id<C>, id<D>, id<E>) -> spawner<A, B, C, D, E>)
& (<A, B, C, D, E, F>(id<A>, id<B>, id<C>, id<D>, id<E>, id<F>) -> spawner<A, B, C, D, E, F>)
& (<A, B, C, D, E, F, G>(id<A>, id<B>, id<C>, id<D>, id<E>, id<F>, id<G>) -> spawner<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>
) -> spawner<A, B, C, D, E, F, G, H>)
& (<A, B, C, D, E, F, G, H, I>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>
) -> spawner<A, B, C, D, E, F, G, H, I>)
& (<A, B, C, D, E, F, G, H, I, J>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>
) -> spawner<A, B, C, D, E, F, G, H, I, J>)
& (<A, B, C, D, E, F, G, H, I, J, K>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K>)
& (<A, B, C, D, E, F, G, H, I, J, K, L>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>,
id<X>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>,
id<X>,
id<Y>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y>)
& (<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z>(
id<A>,
id<B>,
id<C>,
id<D>,
id<E>,
id<F>,
id<G>,
id<H>,
id<I>,
id<J>,
id<K>,
id<L>,
id<M>,
id<N>,
id<O>,
id<P>,
id<Q>,
id<R>,
id<S>,
id<T>,
id<U>,
id<V>,
id<W>,
id<X>,
id<Y>,
id<Z>
) -> spawner<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z>)
return {}

View file

@ -1,6 +1,5 @@
--!strict
--!optimize 2
--[[
original author by @memorycode
@ -28,24 +27,12 @@ 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
export type SignalLike<D, T...> = { connect: Connector<D, T...>, [any]: any } | { Connect: Connector<D, T...>, [any]: any }
type Connector<D, T...> = (self: SignalLike<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)
--- Expects signals to have a `Connect` or `connect` ***method***.
local function collect<D, T...>(event: SignalLike<D, T...>): (() -> (number, T...), D)
local storage = {}
local mt = {}
local iter = function()
@ -53,11 +40,11 @@ local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T.
return function(): (number?, T...)
if n <= 0 then
mt.__iter = nil
return nil
return nil :: any
end
n -= 1
return n + 1, unpack(table.remove(storage, 1) :: any)
return n + 1, unpack(table.remove(storage, 1) :: any) :: any
end
end

View file

@ -0,0 +1,140 @@
--!strict
--!optimize 2
local jecs = require("../../jecs")
type Entity<T = unknown> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type World = jecs.World
export type Identity = {
--- Execute all commands and clear the buffer
flush: () -> (),
--- Peeks into the commands currently stored by the buffer
peek: () -> Commands,
--- Adds a component to the entity with no value
add: <T>(entity: Entity, component: Id<T>) -> (),
--- 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: <T>(entity: Entity, component: Id<T>) -> (),
--- Deletes an entity and all it's related components and relationships
delete: (entity: Entity) -> (),
}
export type Commands = {
add: { [Id]: { Entity } },
set: { [Id]: { [Entity]: unknown } },
remove: { [Id]: { Entity } },
delete: { Entity },
deletion_lookup: { [Entity]: true },
}
local function construct(world: World): Identity
local add_commands: { [Id]: { Entity } } = {}
local set_commands: { [Id]: { [Entity]: unknown } } = {}
local remove_commands: { [Id]: { Entity } } = {}
local delete_commands: { Entity } = {}
-- Double memory usage for deletions but preserve order while keeping O(1) performance for lookups
local deletion_lookup: { [Entity]: true } = {}
local function flush()
for _, entity in delete_commands do
world:delete(entity)
end
for component, entities in add_commands do
for _, entity in entities do
if deletion_lookup[entity] then
continue
end
world:add(entity, component)
end
end
table.clear(add_commands)
for component, entities in set_commands do
for entity, value in entities do
if deletion_lookup[entity] then
continue
end
world:set(entity, component, value)
end
end
table.clear(set_commands)
for component, entities in remove_commands do
for _, entity in entities do
if deletion_lookup[entity] then
continue
end
world:remove(entity, component)
end
end
table.clear(remove_commands)
table.clear(delete_commands)
table.clear(deletion_lookup)
end
local function peek()
return {
add = add_commands,
set = set_commands,
remove = remove_commands,
delete = delete_commands,
deletion_lookup = deletion_lookup,
}
end
local function add<T>(entity: Entity, component: Id<T>)
local cmds = add_commands[component]
if not cmds then
cmds = {}
add_commands[component] = cmds
end
table.insert(cmds, entity)
end
local function set<T>(entity: Entity, component: Id<T>, data: T)
local cmds = set_commands[component]
if not cmds then
cmds = {}
set_commands[component] = cmds
end
cmds[entity] = data
end
local function remove<T>(entity: Entity, component: Id<T>)
local cmds = remove_commands[component]
if not cmds then
cmds = {}
remove_commands[component] = cmds
end
table.insert(cmds, entity)
end
local function delete(entity: Entity)
table.insert(delete_commands, entity)
deletion_lookup[entity] = true
end
return {
flush = flush,
peek = peek,
add = add,
set = set,
remove = remove,
delete = delete,
}
end
return construct

85
lib/utilities/ref.luau Normal file
View file

@ -0,0 +1,85 @@
--!strict
--!optimize 2
local jecs = require("../../jecs")
type Entity<T = unknown> = jecs.Entity<T>
export type Identity = typeof(setmetatable(
{},
{} :: {
__call: <T>(any, key: unknown) -> (Entity<T>, Cleaner),
__index: {
reference: <T>(key: unknown) -> (Entity<T>, Cleaner),
find: <T>(key: unknown) -> (Entity<T>?, Cleaner?),
},
}
))
type Cleaner = () -> ()
local ref_cache: { [jecs.World]: Identity } = {}
local function construct(world: jecs.World, skip_cache: boolean?): Identity
if not skip_cache then
local hit = ref_cache[world]
if hit then
return hit
end
end
local lookup: { [unknown]: Entity } = {}
local cleaner_cache: { [unknown]: Cleaner } = {}
local function serve_cleaner(key: unknown): () -> ()
local hit = cleaner_cache[key]
if hit then
return hit
end
local function cleaner()
lookup[key] = nil
cleaner_cache[key] = nil
end
cleaner_cache[key] = cleaner
return cleaner
end
local function ref<T>(key: unknown): (Entity<T>, Cleaner)
local entity = lookup[key]
if not entity then
entity = world:entity()
lookup[key] = entity
end
return entity, serve_cleaner(key)
end
local function find<T>(key: unknown): (Entity<T>?, Cleaner?)
local entity = lookup[key]
if not entity then
return nil, nil
end
return entity, serve_cleaner(key)
end
local function call<T>(_, key: unknown): (Entity<T>, Cleaner)
return ref(key)
end
local self = setmetatable({}, {
__call = call,
__index = {
reference = ref,
find = find,
},
})
if not skip_cache then
ref_cache[world] = self
end
return self
end
return construct

273
lib/utilities/tracker.luau Normal file
View file

@ -0,0 +1,273 @@
--!strict
--!optimize 2
local command_buffer = require("./command_buffer")
local jecs = require("../../jecs")
type Entity<T = unknown> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type i53 = number
local OnAdd = jecs.OnAdd
local OnSet = jecs.OnSet
local OnRemove = jecs.OnRemove
local construct_ref = require("./ref")
-- The external type differs for better DX
export type Commands = {
added: { [i53]: { i53 } },
set: { [i53]: { [i53]: unknown } },
removed: { [i53]: { i53 } },
}
type Added = { [Id]: { Entity } }
type Set = { [Id]: { [Entity]: unknown } }
type Removed = { [Id]: { Entity } }
type Lookup = { [Id]: { [Entity]: number } }
type InternalCommands = {
added: Added,
set: Set,
removed: Removed,
}
--- Tracks operations on entities for the provided world.
export type Identity = {
--- Gets the current state.
--- A state is a representation of the minimum of commands necessary to produce the current world from a clean slate.
state: () -> Commands,
--- Gets the currently tracked snapshot.
--- A snapshot is a representation of the minimum of commands necessary to produce the current world back from when the last snapshot was taken.
snapshot: () -> Commands?,
--- Applies a set of commands to the tracked world, optionally doing it through a command buffer.
apply: (snapshot: Commands, buf: command_buffer.Identity?) -> (),
}
local function get_non_nilable<T, K>(container: {} & T, index: K): index<T, K>
local data = container[index]
if not data then
data = {}
container[index] = data
end
return data
end
local function insert_unique<T, V>(container: T, value: V, lookup: { [V]: number }?)
if lookup then
if lookup[value] then
return
end
local idx = #lookup + 1
lookup[value] = idx;
(container :: any)[idx] = value
return
end
if table.find(container, value) then
return
end
table.insert(container, value)
end
local function construct(world: jecs.World, ...: Entity<any>): Identity
local components = { ... }
local ref = construct_ref(world, true)
local state_added: Added = {}
local state_added_lookup: Lookup = {}
local state_set: Set = {}
local state_removed: Removed = {}
local state_removed_lookup: Lookup = {}
local snapshot_added: Added = {}
local snapshot_set: Set = {}
local snapshot_removed: Removed = {}
for _, component in components do
world:set(component, OnAdd, function(entity: Entity)
local snapshot = get_non_nilable(snapshot_added, component)
insert_unique(snapshot, entity)
local state = get_non_nilable(state_added, component)
local lookup = get_non_nilable(state_added_lookup, component)
insert_unique(state, entity, lookup)
-- Clean up previous operations
local set_state = state_set[component]
if set_state and set_state[entity] then
set_state[entity] = nil
end
local removed_lookup = state_removed_lookup[component]
if removed_lookup then
local idx = removed_lookup[entity]
if idx then
removed_lookup[entity] = nil
local removed_state = state_removed[component]
if removed_state then
-- Shifting around the array could be expensive, prefer `tbl[idx] = nil`
removed_state[idx] = nil
end
end
end
end)
world:set(component, OnSet, function(entity, value)
local snapshot = get_non_nilable(snapshot_set, component)
snapshot[entity] = value
local state = get_non_nilable(state_set, component)
state[entity] = value
-- Clean up previous operations
local added_lookup = state_added_lookup[component]
if added_lookup then
local idx = added_lookup[entity]
if idx then
added_lookup[entity] = nil
local added_state = state_added[component]
if added_state then
-- Shifting around the array could get expensive, prefer `array[idx] = nil`
added_state[idx] = nil
end
end
end
local removed_lookup = state_removed_lookup[component]
if removed_lookup then
local idx = removed_lookup[entity]
if idx then
removed_lookup[entity] = nil
local removed_state = state_removed[component]
if removed_state then
-- Shifting around the array could get expensive, prefer `array[idx] = nil`
removed_state[idx] = nil
end
end
end
end)
world:set(component, OnRemove, function(entity: Entity)
local snapshot = get_non_nilable(snapshot_removed, component)
insert_unique(snapshot, entity)
local state = get_non_nilable(state_removed, component)
local lookup = get_non_nilable(state_removed_lookup, component)
-- Clean up previous operations
local added_lookup = state_added_lookup[component]
if added_lookup then
local idx = added_lookup[entity]
if idx then
added_lookup[entity] = nil
local added_state = state_added[component]
if added_state then
-- Shifting around the array could get expensive, prefer `array[idx] = nil`
added_state[idx] = nil
end
end
end
local set_state = state_set[component]
if set_state and set_state[entity] then
set_state[entity] = nil
end
insert_unique(state, entity, lookup)
end)
end
-- We cast anything exposing `Commands` as `any` to improve the types for the end user
local function get_state(): InternalCommands
return {
added = state_added,
set = state_set,
removed = state_removed,
}
end
local function get_snapshot(): InternalCommands?
local diff_added = snapshot_added
local diff_set = snapshot_set
local diff_removed = snapshot_removed
snapshot_added = {}
snapshot_set = {}
snapshot_removed = {}
if next(diff_added) == nil and next(diff_set) == nil and next(diff_removed) == nil then
return nil
end
return {
added = diff_added,
set = diff_set,
removed = diff_removed,
}
end
local function apply_snapshot(snapshot: InternalCommands, buf: command_buffer.Identity?)
local add
local set
local remove
do
if buf then
add = buf.add
set = buf.set
remove = buf.remove
else
function add<T>(entity: Entity, component: Id<T>)
world:add(entity, component)
end
function set<T>(entity: Entity, component: Id<T>, data: T)
world:set(entity, component, data)
end
function remove<T>(entity: Entity, component: Id<T>)
world:remove(entity, component)
end
end
end
for component, entities in snapshot.added do
for _, id in entities do
local entity = ref(`foreign-{id}`)
if world:has(entity, component) then
continue
end
add(entity, component)
end
end
for component, entities in snapshot.set do
for id, data in entities do
local entity = ref(`foreign-{id}`)
if world:get(entity, component) == data then
continue
end
set(entity, component, data)
end
end
for component, entities in snapshot.removed do
for _, id in entities do
local entity = ref(`foreign-{id}`)
if world:has(entity, component) then
continue
end
remove(entity, component)
end
end
end
-- Public types differ for better DX
return {
state = get_state,
snapshot = get_snapshot,
apply = apply_snapshot,
} :: any
end
return construct

View file

@ -1,29 +0,0 @@
--!strict
--!optimize 2
local jecs = require("../jecs")
local WORLD: jecs.World
local listeners: { (jecs.World) -> () } = {}
local function get(): jecs.World
return WORLD
end
local function set(world: jecs.World)
WORLD = world
for _, fn in listeners do
fn(world)
end
end
local function on_set(fn: (jecs.World) -> ())
table.insert(listeners, fn)
end
return {
get = get,
set = set,
on_set = on_set,
}

View file

@ -1,10 +0,0 @@
{
"luau-lsp.fflags.override": {
"LuauTinyControlFlowAnalysis": "true"
},
"luau-lsp.require.mode": "relativeToFile",
"luau-lsp.require.fileAliases": {
"@jecs": "Packages/jecs"
},
"luau-lsp.platform.type": "roblox"
}

View file

@ -1,17 +1,18 @@
name = "mark_marks/jecs_utils"
version = "0.1.7-rc.1"
description = "A set of utilities for jecs"
authors = ["marked/Mark-Marks"]
repository = "https://github.com/mark-marks/jecs-utils"
name = "marked/hammer"
version = "0.2.0"
description = "A set of utilities for Jecs"
authors = ["marked"]
repository = "https://git.devmarked.win/marked/hammer"
license = "MIT"
includes = ["lib", "LICENSE", "pesde.toml", "README.md", "jecs.luau"]
includes = ["lib", "lib/**", "LICENSE", "pesde.toml", "README.md", "jecs.luau"]
[target]
environment = "luau"
lib = "lib/init.luau"
[indices]
default = "https://github.com/daimond113/pesde-index"
default = "https://github.com/pesde-pkg/index"
[dependencies]
jecs = { name = "mark_marks/jecs_pesde", version = "^0.4.0" }
# `marked/jecs@0.5.5` was yanked due to some issues, this is the equivalent
jecs = { name = "marked/jecs_nightly", version = "=0.5.5-nightly.20250312T202956Z" }

View file

@ -5,11 +5,9 @@
[tools]
wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4"
lune = "lune-org/lune@0.8.9"
selene = "kampfkarren/selene@0.27.1"
luau-lsp = "johnnymorganz/luau-lsp@1.36.0"
stylua = "johnnymorganz/stylua@0.20.0"
wally-package-types = "johnnymorganz/wally-package-types@1.3.2"
darklua = "seaofvoices/darklua@0.13.1"
pesde = "daimond113/pesde@0.5.0-rc.16"
rojo = "rojo-rbx/rojo@7.5.1"
lune = "lune-org/lune@0.9.2"
selene = "kampfkarren/selene@0.28.0"
luau-lsp = "johnnymorganz/luau-lsp@1.45.0"
stylua = "johnnymorganz/stylua@2.1.0"
pesde = "daimond113/pesde@0.6.2+registry.0.2.2"

View file

@ -1,2 +1 @@
std = "selene_definitions"
exclude = ["lib/jecs.luau"]

View file

@ -3,7 +3,7 @@ line_endings = "Unix"
indent_type = "Spaces"
indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
call_parentheses = "Input"
collapse_simple_statement = "Never"
[sort_requires]

View file

@ -3,19 +3,19 @@
-- https://github.com/red-blox/Util/blob/main/libs/Signal/Signal.luau
-- adapted to work in pure luau
type node<T...> = {
next: node<T...>?,
type Node<T...> = {
next: Node<T...>?,
callback: (T...) -> (),
}
export type signal<T...> = {
root: node<T...>?,
export type Signal<T...> = {
root: Node<T...>?,
connect: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
wait: (self: signal<T...>) -> T...,
once: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
fire: (self: signal<T...>, T...) -> (),
disconnect_all: (self: signal<T...>) -> (),
connect: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
wait: (self: Signal<T...>) -> T...,
once: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
fire: (self: Signal<T...>, T...) -> (),
disconnect_all: (self: Signal<T...>) -> (),
}
local Signal = {}
@ -23,7 +23,7 @@ Signal.__index = Signal
-- Extracted this function from Connect as it results in the closure
-- made in Connect using less memory because this function can be static
local function disconnect<T...>(self: signal<T...>, Node: node<T...>)
local function disconnect<T...>(self: Signal<T...>, Node: Node<T...>)
if self.root == Node then
self.root = Node.next
else
@ -40,7 +40,7 @@ local function disconnect<T...>(self: signal<T...>, Node: node<T...>)
end
end
function Signal.connect<T...>(self: signal<T...>, Callback: (T...) -> ()): () -> ()
function Signal.connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
local node = {
next = self.root,
callback = Callback,
@ -53,30 +53,30 @@ function Signal.connect<T...>(self: signal<T...>, Callback: (T...) -> ()): () ->
end
end
function Signal.wait<T...>(self: signal<T...>): T...
function Signal.wait<T...>(self: Signal<T...>): T...
local Thread = coroutine.running()
local Disconnect
Disconnect = self:connect(function(...)
Disconnect()
(Disconnect :: any)()
coroutine.resume(Thread, ...)
end)
return coroutine.yield()
end
function Signal.once<T...>(self: signal<T...>, Callback: (T...) -> ()): () -> ()
function Signal.once<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
local Disconnect
Disconnect = self:connect(function(...)
Disconnect()
(Disconnect :: any)()
Callback(...)
end)
return Disconnect
end
function Signal.fire<T...>(self: signal<T...>, ...: T...)
function Signal.fire<T...>(self: Signal<T...>, ...: T...)
local Current = self.root
while Current do
@ -85,11 +85,11 @@ function Signal.fire<T...>(self: signal<T...>, ...: T...)
end
end
function Signal.disconnect_all<T...>(self: signal<T...>)
function Signal.disconnect_all<T...>(self: Signal<T...>)
self.root = nil
end
return function<T...>(): signal<T...>
return function<T...>(): Signal<T...>
return setmetatable({
root = nil,
}, Signal) :: any

View file

@ -1,18 +1,15 @@
--!strict
-- stylua: ignore start
local hammer = require("../lib")
local jecs = require("@pkg/jecs")
local jecs_utils = require("@lib/init")
local testkit = require("./testkit")
type entity<T = nil> = jecs.Entity<T>
type Entity<T = unknown> = jecs.Entity<T>
local collect = jecs_utils.collect
local handle = jecs_utils.handle
local replicator = jecs_utils.replicator
local ref = jecs_utils.ref
local ref_search = ref.search
local command_buffer = jecs_utils.command_buffer
local spawner = jecs_utils.spawner
local collect = hammer.collect
local make_tracker = hammer.tracker
local make_ref = hammer.ref
local make_command_buffer = hammer.command_buffer
local signal = require("./signal")
@ -20,9 +17,9 @@ local BENCH, START = testkit.benchmark()
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
TEST("jecs_utils.collect()", function()
TEST("hammer.collect()", function()
do CASE "collects"
local sig: signal.signal<number> = signal()
local sig: signal.Signal<number> = signal()
local flush = collect(sig)
local should = {}
@ -38,169 +35,152 @@ TEST("jecs_utils.collect()", function()
end
end)
TEST("jecs_utils.handle()", function()
do CASE "has"
local world = jecs.World.new()
jecs_utils.initialize(world)
local entity = world:entity()
local tag = world:entity()
world:add(entity, tag)
CHECK(handle(entity):has(tag))
end
do CASE "get"
local world = jecs.World.new()
jecs_utils.initialize(world)
local entity = world:entity()
local component = world:component()
world:set(entity, component, 50)
CHECK(handle(entity):get(component) == 50)
end
do CASE "add"
local world = jecs.World.new()
jecs_utils.initialize(world)
local entity = world:entity()
local tag = world:entity()
handle(entity):add(tag)
CHECK(world:has(entity, tag))
end
do CASE "set"
local world = jecs.World.new()
jecs_utils.initialize(world)
local entity = world:entity()
local component = world:component()
handle(entity):set(component, 50)
CHECK(world:get(entity, component) == 50)
end
do CASE "remove"
local world = jecs.World.new()
jecs_utils.initialize(world)
local entity = world:entity()
local component = world:component()
handle(entity):set(component, 50)
CHECK(world:get(entity, component) == 50)
end
do CASE "delete"
local world = jecs.World.new()
jecs_utils.initialize(world)
local entity = world:entity()
handle(entity):delete()
CHECK(not world:contains(entity))
end
end)
TEST("jecs_utils.ref()", function()
TEST("hammer.ref()", function()
do CASE "set_ref"
local world = jecs.World.new()
jecs_utils.initialize(world)
local ref = make_ref(world, true)
local a: number = ref(1234):id()
local b: number = ref(1234):id()
local a = ref(1234)
local b = ref(1234)
CHECK(a == b)
end
do CASE "search"
do CASE "find"
local world = jecs.World.new()
jecs_utils.initialize(world)
local ref = make_ref(world, true)
local a: number = ref(1234):id()
local b = ref_search(1234)
assert(b) -- give me the type refinements...
CHECK(a == b:id() :: number)
local a = ref(1234)
local b = ref.find(1234)
CHECK(a == b)
end
do CASE "clearer"
do CASE "cleaner"
local world = jecs.World.new()
jecs_utils.initialize(world)
local ref = make_ref(world, true)
local a, a_clear = ref(1234);
(a_clear :: any)()
local a, clean = ref(1234)
clean()
local b = ref(1234)
CHECK(b:id() :: number ~= a:id() :: number)
CHECK(b ~= a)
end
do CASE "caching"
local world = jecs.World.new()
local ref_a = make_ref(world)
local ref_b = make_ref(world)
CHECK(ref_a == ref_b)
local ref_c = make_ref(world, true)
CHECK(ref_c ~= ref_a and ref_c ~= ref_b)
end
end)
TEST("jecs_utils.replicator()", function()
do CASE "propagates difference"
-- TODO! write extensive tests for state operation cleaning
TEST("hammer.tracker()", function()
do CASE "snapshot"
local world = jecs.World.new()
local tag = world:entity()
local component: jecs.Entity<number> = world:component()
local component = world:component() :: Entity<number>
local entity1 = world:entity()
local entity2 = world:entity()
jecs_utils.initialize(world)
local rep = replicator(component, tag)
local tracker = make_tracker(world, component, tag)
world:add(entity1, tag)
world:set(entity2, component, 50)
local difference: jecs_utils.changes = rep.calculate_difference() :: any
CHECK(difference ~= nil)
local snapshot = tracker.snapshot()
CHECK(snapshot ~= nil)
assert(snapshot) -- Refinements
local world2 = jecs.World.new()
local component2: jecs.Entity<number> = world2:component()
local component2 = world2:component() :: Entity<number>
local tag2 = world2:entity()
jecs_utils.initialize(world2)
local rep2 = replicator(component2, tag2)
local tracker2 = make_tracker(world2, component2, tag2)
rep2.apply_difference(difference)
tracker2.apply(snapshot)
CHECK(ref(`replicated-{entity1}`):has(tag2))
CHECK(ref(`replicated-{entity2}`):get(component2) == 50)
CHECK(world:has(entity1, tag2))
CHECK(world:get(entity2, component2) == 50)
end
do CASE "propagates full data"
do CASE "state"
local world = jecs.World.new()
local tag = world:entity()
local component: jecs.Entity<number> = world:component()
local component = world:component() :: Entity<number>
local entity1 = world:entity()
local entity2 = world:entity()
jecs_utils.initialize(world)
local rep = replicator(component, tag)
local tracker = make_tracker(world, component, tag)
world:add(entity1, tag)
world:set(entity2, component, 50)
local full_data = rep.get_full_data()
CHECK(full_data ~= nil)
local state = tracker.state()
CHECK(state ~= nil)
local world2 = jecs.World.new()
local component2: jecs.Entity<number> = world2:component()
local component2 = world2:component() :: Entity<number>
local tag2 = world2:entity()
jecs_utils.initialize(world2)
local rep2 = replicator(component2, tag2)
local tracker2 = make_tracker(world2, component2, tag2)
rep2.apply_difference(full_data)
tracker2.apply(state)
CHECK(ref(`replicated-{entity1}`):has(tag2))
CHECK(ref(`replicated-{entity2}`):get(component2) == 50)
CHECK(world:has(entity1, tag2))
CHECK(world:get(entity2, component2) == 50)
end
do CASE "simplifying"
local world = jecs.World.new()
local component = world:component() :: Entity<number>
local entity = world:entity()
local tracker = make_tracker(world, component)
world:add(entity, component)
do
local state = tracker.state()
CHECK(table.find(state.added[component :: any], entity))
end
world:set(entity, component, 50)
do
local state = tracker.state()
CHECK(not table.find(state.added[component :: any], entity))
CHECK(state.set[component :: any][entity :: any] == 50)
end
world:remove(entity, component)
do
local state = tracker.state()
CHECK(state.set[component :: any][entity :: any] == nil)
CHECK(table.find(state.removed[component :: any], entity))
end
world:add(entity, component)
do
local state = tracker.state()
CHECK(not table.find(state.removed[component :: any], entity))
end
world:remove(entity, component)
do
local state = tracker.state()
CHECK(state.set[component :: any][entity :: any] == nil)
CHECK(table.find(state.removed[component :: any], entity))
end
world:set(entity, component, 50)
do
local state = tracker.state()
CHECK(not table.find(state.removed[component :: any], entity))
CHECK(state.set[component :: any][entity :: any] == 50)
end
end
end)
TEST("jecs_utils.command_buffer", function()
TEST("hammer.command_buffer()", function()
do CASE "add"
local world = jecs.World.new()
jecs_utils.initialize(world)
local command_buffer = make_command_buffer(world)
local tag = world:entity()
local entity = world:entity()
@ -215,7 +195,7 @@ TEST("jecs_utils.command_buffer", function()
do CASE "set"
local world = jecs.World.new()
jecs_utils.initialize(world)
local command_buffer = make_command_buffer(world)
local component = world:component()
local entity = world:entity()
@ -230,7 +210,7 @@ TEST("jecs_utils.command_buffer", function()
do CASE "remove"
local world = jecs.World.new()
jecs_utils.initialize(world)
local command_buffer = make_command_buffer(world)
local component = world:component()
local entity = world:entity()
@ -246,7 +226,7 @@ TEST("jecs_utils.command_buffer", function()
do CASE "delete"
local world = jecs.World.new()
jecs_utils.initialize(world)
local command_buffer = make_command_buffer(world)
local entity = world:entity()
command_buffer.delete(entity)
@ -255,51 +235,32 @@ TEST("jecs_utils.command_buffer", function()
CHECK(not world:contains(entity))
end
end)
TEST("jecs_utils.spawner()", function()
do CASE "spawn"
do CASE "peek"
local world = jecs.World.new()
jecs_utils.initialize(world)
local command_buffer = make_command_buffer(world)
local c1: entity<number> = world:component()
local c2: entity<string> = world:component()
local c3: entity<{}> = world:component()
local tag1 = world:entity()
local entity1 = world:entity()
command_buffer.add(entity1, tag1)
local t1 = world:entity()
local component1 = world:component()
local entity2 = world:entity()
command_buffer.set(entity2, component1, 50)
local entity_spawner = spawner(c1, c2, c3, t1)
local tag2 = world:component()
local entity3 = world:entity()
command_buffer.remove(entity3, tag2)
local tbl = {}
local entity4 = world:component()
command_buffer.delete(entity4)
local idx = entity_spawner.spawn(1234, "abcdef", tbl)
CHECK(world:contains(idx))
CHECK(world:get(idx, c1) == 1234)
CHECK(world:get(idx, c2) == "abcdef")
CHECK(world:get(idx, c3) == tbl)
CHECK(world:has(idx, t1))
end
do CASE "spawn_with_handle"
local world = jecs.World.new()
jecs_utils.initialize(world)
local c1: entity<number> = world:component()
local c2: entity<string> = world:component()
local c3: entity<{}> = world:component()
local t1 = world:entity()
local entity_spawner = spawner(c1, c2, c3, t1)
local tbl = {}
local ent = entity_spawner.spawn_with_handle(1234, "abcdef", tbl)
CHECK(world:contains(ent:id()))
CHECK(ent:get(c1) == 1234)
CHECK(ent:get(c2) == "abcdef")
CHECK(ent:get(c3) == tbl)
CHECK(ent:has(t1))
local commands = command_buffer.peek()
CHECK(table.find(commands.add[tag1], entity1))
CHECK(commands.set[component1][entity2] == 50)
CHECK(table.find(commands.remove[tag2], entity3))
CHECK(table.find(commands.delete, entity4))
CHECK(commands.deletion_lookup[entity4] == true)
end
end)

View file

@ -1,18 +1,18 @@
[package]
name = "mark-marks/jecs-utils"
version = "0.1.7-rc.1"
name = "mark-marks/hammer"
version = "0.2.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
license = "MIT"
exclude = ["**"]
include = [
"default.project.json",
"dist",
"dist/**",
"lib",
"lib/**",
"LICENSE",
"wally.toml",
"README.md",
]
[dependencies]
jecs = "ukendio/jecs@0.4.0"
jecs = "ukendio/jecs@0.5.5"