Cleanup & refactor
This commit is contained in:
parent
d3b6212463
commit
2a6907434a
50 changed files with 937 additions and 4110 deletions
|
@ -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
98
.forgejo/workflows/ci.yml
Normal 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
|
|
@ -7,28 +7,34 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Project
|
- name: Checkout Project
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rokit
|
- 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
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
lune run build
|
lune run build
|
||||||
|
|
||||||
- name: Upload Build Artifact
|
- name: Upload Build Artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: https://git.devmarked.win/actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: build.rbxm
|
path: hammer.rbxm
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
needs: [build]
|
needs: [build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
@ -36,35 +42,32 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download Build
|
- name: Download Build
|
||||||
uses: actions/download-artifact@v3
|
uses: https://git.devmarked.win/actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: build
|
path: build
|
||||||
|
|
||||||
- name: Rename Build
|
|
||||||
run: mv build/build.rbxm jecs_utils.rbxm
|
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: actions/forgejo-release@v2
|
||||||
with:
|
with:
|
||||||
name: Jecs Utils ${{ github.ref_name }}
|
direction: upload
|
||||||
files: |
|
title: Hammer ${{ github.ref_name }}
|
||||||
jecs_utils.rbxm
|
release-dir: build
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish
|
name: Publish
|
||||||
needs: [release]
|
needs: [release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Project
|
- name: Checkout Project
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rokit
|
- name: Install Rokit
|
||||||
uses: CompeyDev/setup-rokit@v0.1.2
|
uses: https://github.com/CompeyDev/setup-rokit@v0.1.2
|
||||||
|
with:
|
||||||
- name: Prepare for Distribution
|
token: ${{ secrets.githubtoken }}
|
||||||
run: |
|
|
||||||
lune run dist
|
|
||||||
|
|
||||||
- name: Wally Login
|
- name: Wally Login
|
||||||
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
90
.github/workflows/ci.yml
vendored
90
.github/workflows/ci.yml
vendored
|
@ -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
|
|
4
.luaurc
4
.luaurc
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"languageMode": "strict",
|
"languageMode": "strict",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"lib": "lib",
|
"pkg": "luau_packages",
|
||||||
"pkg": "luau_packages"
|
"lune": "~/.lune/.typedefs/0.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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/"
|
|
||||||
)
|
|
|
@ -1,7 +1,4 @@
|
||||||
--!strict
|
--!strict
|
||||||
local spawn = require("util/spawn")
|
local spawn = require("./util/spawn")
|
||||||
|
|
||||||
spawn.start("lune run install-packages")
|
spawn.start("rojo build default.project.json -o hammer.rbxm")
|
||||||
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")
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
--!strict
|
--!strict
|
||||||
local spawn = require("util/spawn")
|
local spawn = require("./util/spawn")
|
||||||
|
|
||||||
spawn.start("lune run analyze")
|
|
||||||
spawn.start("stylua lib/")
|
spawn.start("stylua lib/")
|
||||||
spawn.start("selene lib/")
|
spawn.start("selene lib/")
|
||||||
spawn.start("lune run download-jecs")
|
|
||||||
spawn.start("luau test/tests.luau")
|
spawn.start("luau test/tests.luau")
|
||||||
|
|
|
@ -1,57 +1,10 @@
|
||||||
--!strict
|
--!strict
|
||||||
local process = require("@lune/process")
|
|
||||||
local stdio = require("@lune/stdio")
|
|
||||||
local task = require("@lune/task")
|
local task = require("@lune/task")
|
||||||
|
|
||||||
local spawn = require("util/spawn")
|
local spawn = require("./util/spawn")
|
||||||
local watch = require("util/watch")
|
local watch = require("./util/watch")
|
||||||
|
|
||||||
task.spawn(watch, "wally.toml", function()
|
task.spawn(watch, "pesde.toml", function()
|
||||||
spawn.spawn("lune run install-packages")
|
spawn.spawn("pesde install")
|
||||||
end, false)
|
end, false)
|
||||||
spawn.start("lune run install-packages")
|
spawn.start("pesde install")
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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`
|
|
||||||
)
|
|
|
@ -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/")
|
|
|
@ -9,15 +9,15 @@ local task = require("@lune/task")
|
||||||
--- @param cmd string
|
--- @param cmd string
|
||||||
--- @param options process.SpawnOptions?
|
--- @param options process.SpawnOptions?
|
||||||
--- @return process.SpawnResult
|
--- @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 arguments = string.split(cmd, " ")
|
||||||
local command = arguments[1]
|
local command = arguments[1]
|
||||||
table.remove(arguments, 1)
|
table.remove(arguments, 1)
|
||||||
|
|
||||||
local opts: process.SpawnOptions = options ~= nil and options or {}
|
local opts: process.ExecOptions = options ~= nil and options or {}
|
||||||
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward"
|
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward" :: any
|
||||||
|
|
||||||
return process.spawn(command, arguments, opts)
|
return process.exec(command, arguments, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- `task.spawn` a process with the given command and options
|
--- `task.spawn` a process with the given command and options
|
||||||
|
@ -27,7 +27,7 @@ end
|
||||||
--- @param cmd string
|
--- @param cmd string
|
||||||
--- @param options process.SpawnOptions?
|
--- @param options process.SpawnOptions?
|
||||||
--- @return process.SpawnResult
|
--- @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)
|
task.spawn(start_process, cmd, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
// Folder-specific settings
|
// Folder-specific settings
|
||||||
//
|
//
|
||||||
// For a full list of overridable settings, and general information on 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": {
|
"lsp": {
|
||||||
"luau-lsp": {
|
"luau-lsp": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"luau-lsp": {
|
"luau-lsp": {
|
||||||
|
"diagnostics": {
|
||||||
|
"workspace": false
|
||||||
|
},
|
||||||
"completion": {
|
"completion": {
|
||||||
"imports": {
|
"imports": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"suggestServices": true,
|
"suggestServices": true,
|
||||||
"suggestRequires": false
|
"suggestRequires": true,
|
||||||
|
"stringRequires": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"sourcemap": {
|
|
||||||
"rojoProjectFile": "dev.project.json"
|
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"mode": "relativeToFile"
|
"mode": "relativeToFile"
|
||||||
|
@ -23,22 +26,22 @@
|
||||||
},
|
},
|
||||||
"ext": {
|
"ext": {
|
||||||
"roblox": {
|
"roblox": {
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
|
"security_level": "roblox_script"
|
||||||
},
|
},
|
||||||
"fflags": {
|
"fflags": {
|
||||||
"override": {
|
"enable_new_solver": true,
|
||||||
"LuauTinyControlFlowAnalysis": "true"
|
|
||||||
},
|
|
||||||
"sync": true,
|
"sync": true,
|
||||||
"enable_by_default": false
|
"enable_by_default": false
|
||||||
|
},
|
||||||
|
"binary": {
|
||||||
|
"ignore_system_version": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"languages": {
|
"file_types": {
|
||||||
"TypeScript": {
|
"Luau": ["lua"]
|
||||||
"tab_size": 4
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Mark-Marks
|
Copyright (c) 2025 marked
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
117
README.md
117
README.md
|
@ -1,18 +1,113 @@
|
||||||
# sapphire-utils
|
<p align="center">
|
||||||
[](https://github.com/mark-marks/jecs-utils/actions/workflows/ci.yml)
|
<img src="assets/hammer-logo.png">
|
||||||
[](https://wally.run/package/mark-marks/jecs-utils)
|
</p>
|
||||||
[](https://github.com/Mark-Marks/jecs-utils/blob/main/LICENSE)
|
|
||||||
|
[](https://git.devmarked.win/marked/hammer/actions?workflow=ci.yml)
|
||||||
|
[](https://git.devmarked.win/marked/hammer/actions?workflow=release.yml)
|
||||||
|
[](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)
|
A set of utilities for [Jecs](https://github.com/ukendio/jecs)
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Features
|
## Installation
|
||||||
|
|
||||||
- [collect](/lib/collect.luau) - Collects all arguments fired through the given signal, and drains the collection on iteration.
|
Hammer is available on pesde @ `marked/hammer` and Wally @ `mark-marks/hammer`.
|
||||||
- [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
|
## Usage
|
||||||
- [ref](/lib/ref.luau) - Reference entities by key
|
|
||||||
- [command_buffer](/lib/command_buffer.luau) - Buffer commands to prevent iterator invalidation
|
All utilities that require a Jecs world to function are exposed via a constructor pattern.\
|
||||||
- [spawner](/lib/spawner.luau) - Spawn entities with required components
|
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
BIN
assets/hammer-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "jecs-utils",
|
"name": "hammer",
|
||||||
"tree": {
|
"tree": {
|
||||||
"$path": "dist"
|
"$path": "lib"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "dev",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
|
|
||||||
"ReplicatedStorage": {
|
|
||||||
"Packages": {
|
|
||||||
"$className": "Folder",
|
|
||||||
"$path": "Packages",
|
|
||||||
"jecs": {
|
|
||||||
"$path": "jecs.luau"
|
|
||||||
},
|
|
||||||
"jecs_utils": {
|
|
||||||
"$path": "lib"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,8 +6,8 @@
|
||||||
-- this is here to mitigate that
|
-- this is here to mitigate that
|
||||||
local jecs = require("./luau_packages/jecs")
|
local jecs = require("./luau_packages/jecs")
|
||||||
export type Archetype = jecs.Archetype
|
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 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
|
export type World = jecs.World
|
||||||
return jecs
|
return jecs
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,42 +1,22 @@
|
||||||
--!strict
|
--!strict
|
||||||
--!optimize 2
|
--!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")
|
local tracker = require("@self/utilities/tracker")
|
||||||
export type collect_signal_like<T...> = collect.signal_like<any, T...>
|
export type Tracker = tracker.Identity
|
||||||
export type collect_verbose_signal_like<D, T...> = collect.signal_like<D, T...>
|
export type TrackerCommands = tracker.Commands
|
||||||
|
|
||||||
local command_buffer = require("./command_buffer")
|
local command_buffer = require("@self/utilities/command_buffer")
|
||||||
export type command_buffer = command_buffer.command_buffer
|
export type CommandBuffer = command_buffer.Identity
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialize = initialize,
|
|
||||||
|
|
||||||
collect = collect,
|
collect = collect,
|
||||||
handle = handle,
|
|
||||||
replicator = replicator,
|
|
||||||
ref = ref,
|
ref = ref,
|
||||||
|
tracker = tracker,
|
||||||
command_buffer = command_buffer,
|
command_buffer = command_buffer,
|
||||||
spawner = spawner,
|
|
||||||
}
|
}
|
||||||
|
|
67
lib/ref.luau
67
lib/ref.luau
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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 {}
|
|
|
@ -1,6 +1,5 @@
|
||||||
--!strict
|
--!strict
|
||||||
--!optimize 2
|
--!optimize 2
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
original author by @memorycode
|
original author by @memorycode
|
||||||
|
|
||||||
|
@ -28,24 +27,12 @@ SOFTWARE.
|
||||||
--]]
|
--]]
|
||||||
|
|
||||||
--- What signals passed to `collect()` should be able to be coerced into
|
--- 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 }
|
export type SignalLike<D, T...> = { connect: Connector<D, T...>, [any]: any } | { Connect: Connector<D, T...>, [any]: any }
|
||||||
type confn<D, T...> = (self: signal_like<D, T...>, (T...) -> ()) -> D
|
type Connector<D, T...> = (self: SignalLike<D, T...>, (T...) -> ()) -> D
|
||||||
|
|
||||||
--- Collects all arguments fired through the given signal, and drains the collection on iteration.\
|
--- Collects all arguments fired through the given signal, and drains the collection on iteration.\
|
||||||
--- Expects signals to have a `Connect` ***method***.
|
--- Expects signals to have a `Connect` or `connect` ***method***.
|
||||||
--- ```luau
|
local function collect<D, T...>(event: SignalLike<D, T...>): (() -> (number, T...), D)
|
||||||
--- local sig = collect(some_signal)
|
|
||||||
---
|
|
||||||
--- -- Imagine this as an ECS scheduler loop
|
|
||||||
--- while task.wait() do
|
|
||||||
--- for index, arg1 in sig do -- arg1, arg2, etc
|
|
||||||
--- print(arg1)
|
|
||||||
--- end
|
|
||||||
--- end
|
|
||||||
--- ```
|
|
||||||
--- @param event signal<T...>
|
|
||||||
--- @return () -> (number, T...), D -- iterator and disconnector
|
|
||||||
local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T...), D)
|
|
||||||
local storage = {}
|
local storage = {}
|
||||||
local mt = {}
|
local mt = {}
|
||||||
local iter = function()
|
local iter = function()
|
||||||
|
@ -53,11 +40,11 @@ local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T.
|
||||||
return function(): (number?, T...)
|
return function(): (number?, T...)
|
||||||
if n <= 0 then
|
if n <= 0 then
|
||||||
mt.__iter = nil
|
mt.__iter = nil
|
||||||
return nil
|
return nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
n -= 1
|
n -= 1
|
||||||
return n + 1, unpack(table.remove(storage, 1) :: any)
|
return n + 1, unpack(table.remove(storage, 1) :: any) :: any
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
140
lib/utilities/command_buffer.luau
Normal file
140
lib/utilities/command_buffer.luau
Normal 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
85
lib/utilities/ref.luau
Normal 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
273
lib/utilities/tracker.luau
Normal 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
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
17
pesde.toml
17
pesde.toml
|
@ -1,17 +1,18 @@
|
||||||
name = "mark_marks/jecs_utils"
|
name = "marked/hammer"
|
||||||
version = "0.1.7-rc.1"
|
version = "0.2.0"
|
||||||
description = "A set of utilities for jecs"
|
description = "A set of utilities for Jecs"
|
||||||
authors = ["marked/Mark-Marks"]
|
authors = ["marked"]
|
||||||
repository = "https://github.com/mark-marks/jecs-utils"
|
repository = "https://git.devmarked.win/marked/hammer"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
includes = ["lib", "LICENSE", "pesde.toml", "README.md", "jecs.luau"]
|
includes = ["lib", "lib/**", "LICENSE", "pesde.toml", "README.md", "jecs.luau"]
|
||||||
|
|
||||||
[target]
|
[target]
|
||||||
environment = "luau"
|
environment = "luau"
|
||||||
lib = "lib/init.luau"
|
lib = "lib/init.luau"
|
||||||
|
|
||||||
[indices]
|
[indices]
|
||||||
default = "https://github.com/daimond113/pesde-index"
|
default = "https://github.com/pesde-pkg/index"
|
||||||
|
|
||||||
[dependencies]
|
[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" }
|
||||||
|
|
14
rokit.toml
14
rokit.toml
|
@ -5,11 +5,9 @@
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
wally = "upliftgames/wally@0.3.2"
|
wally = "upliftgames/wally@0.3.2"
|
||||||
rojo = "rojo-rbx/rojo@7.4.4"
|
rojo = "rojo-rbx/rojo@7.5.1"
|
||||||
lune = "lune-org/lune@0.8.9"
|
lune = "lune-org/lune@0.9.2"
|
||||||
selene = "kampfkarren/selene@0.27.1"
|
selene = "kampfkarren/selene@0.28.0"
|
||||||
luau-lsp = "johnnymorganz/luau-lsp@1.36.0"
|
luau-lsp = "johnnymorganz/luau-lsp@1.45.0"
|
||||||
stylua = "johnnymorganz/stylua@0.20.0"
|
stylua = "johnnymorganz/stylua@2.1.0"
|
||||||
wally-package-types = "johnnymorganz/wally-package-types@1.3.2"
|
pesde = "daimond113/pesde@0.6.2+registry.0.2.2"
|
||||||
darklua = "seaofvoices/darklua@0.13.1"
|
|
||||||
pesde = "daimond113/pesde@0.5.0-rc.16"
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
std = "selene_definitions"
|
std = "selene_definitions"
|
||||||
exclude = ["lib/jecs.luau"]
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ line_endings = "Unix"
|
||||||
indent_type = "Spaces"
|
indent_type = "Spaces"
|
||||||
indent_width = 4
|
indent_width = 4
|
||||||
quote_style = "AutoPreferDouble"
|
quote_style = "AutoPreferDouble"
|
||||||
call_parentheses = "Always"
|
call_parentheses = "Input"
|
||||||
collapse_simple_statement = "Never"
|
collapse_simple_statement = "Never"
|
||||||
|
|
||||||
[sort_requires]
|
[sort_requires]
|
||||||
|
|
|
@ -3,19 +3,19 @@
|
||||||
-- https://github.com/red-blox/Util/blob/main/libs/Signal/Signal.luau
|
-- https://github.com/red-blox/Util/blob/main/libs/Signal/Signal.luau
|
||||||
-- adapted to work in pure luau
|
-- adapted to work in pure luau
|
||||||
|
|
||||||
type node<T...> = {
|
type Node<T...> = {
|
||||||
next: node<T...>?,
|
next: Node<T...>?,
|
||||||
callback: (T...) -> (),
|
callback: (T...) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
export type signal<T...> = {
|
export type Signal<T...> = {
|
||||||
root: node<T...>?,
|
root: Node<T...>?,
|
||||||
|
|
||||||
connect: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
connect: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
||||||
wait: (self: signal<T...>) -> T...,
|
wait: (self: Signal<T...>) -> T...,
|
||||||
once: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
once: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
||||||
fire: (self: signal<T...>, T...) -> (),
|
fire: (self: Signal<T...>, T...) -> (),
|
||||||
disconnect_all: (self: signal<T...>) -> (),
|
disconnect_all: (self: Signal<T...>) -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
local Signal = {}
|
local Signal = {}
|
||||||
|
@ -23,7 +23,7 @@ Signal.__index = Signal
|
||||||
|
|
||||||
-- Extracted this function from Connect as it results in the closure
|
-- Extracted this function from Connect as it results in the closure
|
||||||
-- made in Connect using less memory because this function can be static
|
-- 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
|
if self.root == Node then
|
||||||
self.root = Node.next
|
self.root = Node.next
|
||||||
else
|
else
|
||||||
|
@ -40,7 +40,7 @@ local function disconnect<T...>(self: signal<T...>, Node: node<T...>)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal.connect<T...>(self: signal<T...>, Callback: (T...) -> ()): () -> ()
|
function Signal.connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
|
||||||
local node = {
|
local node = {
|
||||||
next = self.root,
|
next = self.root,
|
||||||
callback = Callback,
|
callback = Callback,
|
||||||
|
@ -53,30 +53,30 @@ function Signal.connect<T...>(self: signal<T...>, Callback: (T...) -> ()): () ->
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal.wait<T...>(self: signal<T...>): T...
|
function Signal.wait<T...>(self: Signal<T...>): T...
|
||||||
local Thread = coroutine.running()
|
local Thread = coroutine.running()
|
||||||
local Disconnect
|
local Disconnect
|
||||||
|
|
||||||
Disconnect = self:connect(function(...)
|
Disconnect = self:connect(function(...)
|
||||||
Disconnect()
|
(Disconnect :: any)()
|
||||||
coroutine.resume(Thread, ...)
|
coroutine.resume(Thread, ...)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return coroutine.yield()
|
return coroutine.yield()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal.once<T...>(self: signal<T...>, Callback: (T...) -> ()): () -> ()
|
function Signal.once<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
|
||||||
local Disconnect
|
local Disconnect
|
||||||
|
|
||||||
Disconnect = self:connect(function(...)
|
Disconnect = self:connect(function(...)
|
||||||
Disconnect()
|
(Disconnect :: any)()
|
||||||
Callback(...)
|
Callback(...)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return Disconnect
|
return Disconnect
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal.fire<T...>(self: signal<T...>, ...: T...)
|
function Signal.fire<T...>(self: Signal<T...>, ...: T...)
|
||||||
local Current = self.root
|
local Current = self.root
|
||||||
|
|
||||||
while Current do
|
while Current do
|
||||||
|
@ -85,11 +85,11 @@ function Signal.fire<T...>(self: signal<T...>, ...: T...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Signal.disconnect_all<T...>(self: signal<T...>)
|
function Signal.disconnect_all<T...>(self: Signal<T...>)
|
||||||
self.root = nil
|
self.root = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return function<T...>(): signal<T...>
|
return function<T...>(): Signal<T...>
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
root = nil,
|
root = nil,
|
||||||
}, Signal) :: any
|
}, Signal) :: any
|
||||||
|
|
281
test/tests.luau
281
test/tests.luau
|
@ -1,18 +1,15 @@
|
||||||
--!strict
|
--!strict
|
||||||
-- stylua: ignore start
|
-- stylua: ignore start
|
||||||
|
local hammer = require("../lib")
|
||||||
local jecs = require("@pkg/jecs")
|
local jecs = require("@pkg/jecs")
|
||||||
local jecs_utils = require("@lib/init")
|
|
||||||
local testkit = require("./testkit")
|
local testkit = require("./testkit")
|
||||||
|
|
||||||
type entity<T = nil> = jecs.Entity<T>
|
type Entity<T = unknown> = jecs.Entity<T>
|
||||||
|
|
||||||
local collect = jecs_utils.collect
|
local collect = hammer.collect
|
||||||
local handle = jecs_utils.handle
|
local make_tracker = hammer.tracker
|
||||||
local replicator = jecs_utils.replicator
|
local make_ref = hammer.ref
|
||||||
local ref = jecs_utils.ref
|
local make_command_buffer = hammer.command_buffer
|
||||||
local ref_search = ref.search
|
|
||||||
local command_buffer = jecs_utils.command_buffer
|
|
||||||
local spawner = jecs_utils.spawner
|
|
||||||
|
|
||||||
local signal = require("./signal")
|
local signal = require("./signal")
|
||||||
|
|
||||||
|
@ -20,9 +17,9 @@ local BENCH, START = testkit.benchmark()
|
||||||
|
|
||||||
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
|
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
|
||||||
|
|
||||||
TEST("jecs_utils.collect()", function()
|
TEST("hammer.collect()", function()
|
||||||
do CASE "collects"
|
do CASE "collects"
|
||||||
local sig: signal.signal<number> = signal()
|
local sig: signal.Signal<number> = signal()
|
||||||
local flush = collect(sig)
|
local flush = collect(sig)
|
||||||
local should = {}
|
local should = {}
|
||||||
|
|
||||||
|
@ -38,169 +35,152 @@ TEST("jecs_utils.collect()", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("jecs_utils.handle()", function()
|
TEST("hammer.ref()", 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()
|
|
||||||
do CASE "set_ref"
|
do CASE "set_ref"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local ref = make_ref(world, true)
|
||||||
|
|
||||||
local a: number = ref(1234):id()
|
local a = ref(1234)
|
||||||
local b: number = ref(1234):id()
|
local b = ref(1234)
|
||||||
CHECK(a == b)
|
CHECK(a == b)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "search"
|
do CASE "find"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local ref = make_ref(world, true)
|
||||||
|
|
||||||
local a: number = ref(1234):id()
|
local a = ref(1234)
|
||||||
local b = ref_search(1234)
|
local b = ref.find(1234)
|
||||||
assert(b) -- give me the type refinements...
|
CHECK(a == b)
|
||||||
CHECK(a == b:id() :: number)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "clearer"
|
do CASE "cleaner"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local ref = make_ref(world, true)
|
||||||
|
|
||||||
local a, a_clear = ref(1234);
|
local a, clean = ref(1234)
|
||||||
(a_clear :: any)()
|
clean()
|
||||||
local b = ref(1234)
|
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
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("jecs_utils.replicator()", function()
|
-- TODO! write extensive tests for state operation cleaning
|
||||||
do CASE "propagates difference"
|
TEST("hammer.tracker()", function()
|
||||||
|
do CASE "snapshot"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local tag = world:entity()
|
local tag = world:entity()
|
||||||
local component: jecs.Entity<number> = world:component()
|
local component = world:component() :: Entity<number>
|
||||||
|
|
||||||
local entity1 = world:entity()
|
local entity1 = world:entity()
|
||||||
local entity2 = world:entity()
|
local entity2 = world:entity()
|
||||||
|
|
||||||
jecs_utils.initialize(world)
|
local tracker = make_tracker(world, component, tag)
|
||||||
local rep = replicator(component, tag)
|
|
||||||
|
|
||||||
world:add(entity1, tag)
|
world:add(entity1, tag)
|
||||||
world:set(entity2, component, 50)
|
world:set(entity2, component, 50)
|
||||||
|
|
||||||
local difference: jecs_utils.changes = rep.calculate_difference() :: any
|
local snapshot = tracker.snapshot()
|
||||||
CHECK(difference ~= nil)
|
CHECK(snapshot ~= nil)
|
||||||
|
assert(snapshot) -- Refinements
|
||||||
|
|
||||||
local world2 = jecs.World.new()
|
local world2 = jecs.World.new()
|
||||||
local component2: jecs.Entity<number> = world2:component()
|
local component2 = world2:component() :: Entity<number>
|
||||||
local tag2 = world2:entity()
|
local tag2 = world2:entity()
|
||||||
|
|
||||||
jecs_utils.initialize(world2)
|
local tracker2 = make_tracker(world2, component2, tag2)
|
||||||
local rep2 = replicator(component2, tag2)
|
|
||||||
|
|
||||||
rep2.apply_difference(difference)
|
tracker2.apply(snapshot)
|
||||||
|
|
||||||
CHECK(ref(`replicated-{entity1}`):has(tag2))
|
CHECK(world:has(entity1, tag2))
|
||||||
CHECK(ref(`replicated-{entity2}`):get(component2) == 50)
|
CHECK(world:get(entity2, component2) == 50)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "propagates full data"
|
do CASE "state"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local tag = world:entity()
|
local tag = world:entity()
|
||||||
local component: jecs.Entity<number> = world:component()
|
local component = world:component() :: Entity<number>
|
||||||
|
|
||||||
local entity1 = world:entity()
|
local entity1 = world:entity()
|
||||||
local entity2 = world:entity()
|
local entity2 = world:entity()
|
||||||
|
|
||||||
jecs_utils.initialize(world)
|
local tracker = make_tracker(world, component, tag)
|
||||||
local rep = replicator(component, tag)
|
|
||||||
|
|
||||||
world:add(entity1, tag)
|
world:add(entity1, tag)
|
||||||
world:set(entity2, component, 50)
|
world:set(entity2, component, 50)
|
||||||
|
|
||||||
local full_data = rep.get_full_data()
|
local state = tracker.state()
|
||||||
CHECK(full_data ~= nil)
|
CHECK(state ~= nil)
|
||||||
|
|
||||||
local world2 = jecs.World.new()
|
local world2 = jecs.World.new()
|
||||||
local component2: jecs.Entity<number> = world2:component()
|
local component2 = world2:component() :: Entity<number>
|
||||||
local tag2 = world2:entity()
|
local tag2 = world2:entity()
|
||||||
|
|
||||||
jecs_utils.initialize(world2)
|
local tracker2 = make_tracker(world2, component2, tag2)
|
||||||
local rep2 = replicator(component2, tag2)
|
|
||||||
|
|
||||||
rep2.apply_difference(full_data)
|
tracker2.apply(state)
|
||||||
|
|
||||||
CHECK(ref(`replicated-{entity1}`):has(tag2))
|
CHECK(world:has(entity1, tag2))
|
||||||
CHECK(ref(`replicated-{entity2}`):get(component2) == 50)
|
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
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("jecs_utils.command_buffer", function()
|
TEST("hammer.command_buffer()", function()
|
||||||
do CASE "add"
|
do CASE "add"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local command_buffer = make_command_buffer(world)
|
||||||
|
|
||||||
local tag = world:entity()
|
local tag = world:entity()
|
||||||
local entity = world:entity()
|
local entity = world:entity()
|
||||||
|
@ -215,7 +195,7 @@ TEST("jecs_utils.command_buffer", function()
|
||||||
|
|
||||||
do CASE "set"
|
do CASE "set"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local command_buffer = make_command_buffer(world)
|
||||||
|
|
||||||
local component = world:component()
|
local component = world:component()
|
||||||
local entity = world:entity()
|
local entity = world:entity()
|
||||||
|
@ -230,7 +210,7 @@ TEST("jecs_utils.command_buffer", function()
|
||||||
|
|
||||||
do CASE "remove"
|
do CASE "remove"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local command_buffer = make_command_buffer(world)
|
||||||
|
|
||||||
local component = world:component()
|
local component = world:component()
|
||||||
local entity = world:entity()
|
local entity = world:entity()
|
||||||
|
@ -246,7 +226,7 @@ TEST("jecs_utils.command_buffer", function()
|
||||||
|
|
||||||
do CASE "delete"
|
do CASE "delete"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local command_buffer = make_command_buffer(world)
|
||||||
|
|
||||||
local entity = world:entity()
|
local entity = world:entity()
|
||||||
command_buffer.delete(entity)
|
command_buffer.delete(entity)
|
||||||
|
@ -255,51 +235,32 @@ TEST("jecs_utils.command_buffer", function()
|
||||||
|
|
||||||
CHECK(not world:contains(entity))
|
CHECK(not world:contains(entity))
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
TEST("jecs_utils.spawner()", function()
|
do CASE "peek"
|
||||||
do CASE "spawn"
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
jecs_utils.initialize(world)
|
local command_buffer = make_command_buffer(world)
|
||||||
|
|
||||||
local c1: entity<number> = world:component()
|
local tag1 = world:entity()
|
||||||
local c2: entity<string> = world:component()
|
local entity1 = world:entity()
|
||||||
local c3: entity<{}> = world:component()
|
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)
|
local commands = command_buffer.peek()
|
||||||
CHECK(world:contains(idx))
|
CHECK(table.find(commands.add[tag1], entity1))
|
||||||
CHECK(world:get(idx, c1) == 1234)
|
CHECK(commands.set[component1][entity2] == 50)
|
||||||
CHECK(world:get(idx, c2) == "abcdef")
|
CHECK(table.find(commands.remove[tag2], entity3))
|
||||||
CHECK(world:get(idx, c3) == tbl)
|
CHECK(table.find(commands.delete, entity4))
|
||||||
CHECK(world:has(idx, t1))
|
CHECK(commands.deletion_lookup[entity4] == true)
|
||||||
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))
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
10
wally.toml
10
wally.toml
|
@ -1,18 +1,18 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mark-marks/jecs-utils"
|
name = "mark-marks/hammer"
|
||||||
version = "0.1.7-rc.1"
|
version = "0.2.0"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
exclude = ["**"]
|
exclude = ["**"]
|
||||||
include = [
|
include = [
|
||||||
"default.project.json",
|
"default.project.json",
|
||||||
"dist",
|
"lib",
|
||||||
"dist/**",
|
"lib/**",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"wally.toml",
|
"wally.toml",
|
||||||
"README.md",
|
"README.md",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jecs = "ukendio/jecs@0.4.0"
|
jecs = "ukendio/jecs@0.5.5"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue