Compare commits
No commits in common. "2a6907434a040b89074354ef5de86099dfa967bc" and "b4cc94f369ca4508f85a717763521d35f46f2a51" have entirely different histories.
2a6907434a
...
b4cc94f369
51 changed files with 4160 additions and 1002 deletions
17
.darklua.json
Normal file
17
.darklua.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"process": [
|
||||
{
|
||||
"rule": "convert_require",
|
||||
"current": {
|
||||
"name": "path",
|
||||
"sources": {
|
||||
"@jecs": "Packages/jecs"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"name": "roblox",
|
||||
"rojo_sourcemap": "sourcemap.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
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
|
85
.github/workflows/ci.yml
vendored
Normal file
85
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,85 @@
|
|||
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: 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
|
|
@ -7,34 +7,28 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
lune run build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: https://git.devmarked.win/actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: hammer.rbxm
|
||||
path: build.rbxm
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: [build]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
|
@ -42,32 +36,35 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Build
|
||||
uses: https://git.devmarked.win/actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- name: Rename Build
|
||||
run: mv build/build.rbxm jecs_utils.rbxm
|
||||
|
||||
- name: Create Release
|
||||
uses: actions/forgejo-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
direction: upload
|
||||
title: Hammer ${{ github.ref_name }}
|
||||
release-dir: build
|
||||
name: Jecs Utils ${{ github.ref_name }}
|
||||
files: |
|
||||
jecs_utils.rbxm
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
needs: [release]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||
runs-on: ubuntu-latest
|
||||
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 }}
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Prepare for Distribution
|
||||
run: |
|
||||
lune run dist
|
||||
|
||||
- name: Wally Login
|
||||
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
||||
|
@ -76,7 +73,7 @@ jobs:
|
|||
run: wally publish
|
||||
|
||||
- name: Pesde Login
|
||||
run: pesde auth login --token "${{ secrets.PESDE_AUTH_TOKEN }}"
|
||||
run: pesde auth login --token "Bearer ${{ secrets.WALLY_AUTH_TOKEN }}"
|
||||
|
||||
- name: Pesde Publish
|
||||
run: pesde publish -y
|
||||
run: pesde publish
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -44,9 +44,7 @@ dist/
|
|||
# Wally files
|
||||
DevPackages
|
||||
Packages
|
||||
luau_packages
|
||||
wally.lock
|
||||
pesde.lock
|
||||
WallyPatches
|
||||
|
||||
# Typescript
|
||||
|
@ -57,6 +55,7 @@ WallyPatches
|
|||
roblox.toml
|
||||
sourcemap.json
|
||||
globalTypes.d.luau
|
||||
jecs_src.luau
|
||||
# Used for testing, unfortunately we can't just install it with wally and expect it to work in vanilla luau
|
||||
jecs.luau
|
||||
# Stores Github PAT
|
||||
.env
|
||||
|
|
5
.luaurc
5
.luaurc
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"languageMode": "strict",
|
||||
"aliases": {
|
||||
"pkg": "luau_packages",
|
||||
"lune": "~/.lune/.typedefs/0.9.2"
|
||||
"jecs_utils": "lib",
|
||||
"testkit": "test/testkit",
|
||||
"jecs": "jecs"
|
||||
}
|
||||
}
|
||||
|
|
408
.lune/.lune-defs/datetime.luau
Normal file
408
.lune/.lune-defs/datetime.luau
Normal file
|
@ -0,0 +1,408 @@
|
|||
--[[
|
||||
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
|
289
.lune/.lune-defs/fs.luau
Normal file
289
.lune/.lune-defs/fs.luau
Normal file
|
@ -0,0 +1,289 @@
|
|||
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
|
123
.lune/.lune-defs/luau.luau
Normal file
123
.lune/.lune-defs/luau.luau
Normal file
|
@ -0,0 +1,123 @@
|
|||
--[=[
|
||||
@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
|
321
.lune/.lune-defs/net.luau
Normal file
321
.lune/.lune-defs/net.luau
Normal file
|
@ -0,0 +1,321 @@
|
|||
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
|
182
.lune/.lune-defs/process.luau
Normal file
182
.lune/.lune-defs/process.luau
Normal file
|
@ -0,0 +1,182 @@
|
|||
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
|
218
.lune/.lune-defs/regex.luau
Normal file
218
.lune/.lune-defs/regex.luau
Normal file
|
@ -0,0 +1,218 @@
|
|||
--[=[
|
||||
@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
|
507
.lune/.lune-defs/roblox.luau
Normal file
507
.lune/.lune-defs/roblox.luau
Normal file
|
@ -0,0 +1,507 @@
|
|||
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
|
200
.lune/.lune-defs/serde.luau
Normal file
200
.lune/.lune-defs/serde.luau
Normal file
|
@ -0,0 +1,200 @@
|
|||
--[=[
|
||||
@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
|
161
.lune/.lune-defs/stdio.luau
Normal file
161
.lune/.lune-defs/stdio.luau
Normal file
|
@ -0,0 +1,161 @@
|
|||
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
|
99
.lune/.lune-defs/task.luau
Normal file
99
.lune/.lune-defs/task.luau
Normal file
|
@ -0,0 +1,99 @@
|
|||
--[=[
|
||||
@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
|
8
.lune/analyze.luau
Normal file
8
.lune/analyze.luau
Normal file
|
@ -0,0 +1,8 @@
|
|||
--!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,4 +1,7 @@
|
|||
--!strict
|
||||
local spawn = require("./util/spawn")
|
||||
local spawn = require("util/spawn")
|
||||
|
||||
spawn.start("rojo build default.project.json -o hammer.rbxm")
|
||||
spawn.start("lune run install-packages")
|
||||
spawn.start("rojo sourcemap dev.project.json -o sourcemap.json")
|
||||
spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } })
|
||||
spawn.start("rojo build default.project.json -o build.rbxm")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
--!strict
|
||||
local spawn = require("./util/spawn")
|
||||
local spawn = require("util/spawn")
|
||||
|
||||
spawn.start("lune run analyze")
|
||||
spawn.start("stylua lib/")
|
||||
spawn.start("selene lib/")
|
||||
spawn.start("lune run download-jecs")
|
||||
spawn.start("luau test/tests.luau")
|
||||
|
|
|
@ -1,10 +1,57 @@
|
|||
--!strict
|
||||
local process = require("@lune/process")
|
||||
local stdio = require("@lune/stdio")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local spawn = require("./util/spawn")
|
||||
local watch = require("./util/watch")
|
||||
local spawn = require("util/spawn")
|
||||
local watch = require("util/watch")
|
||||
|
||||
task.spawn(watch, "pesde.toml", function()
|
||||
spawn.spawn("pesde install")
|
||||
task.spawn(watch, "wally.toml", function()
|
||||
spawn.spawn("lune run install-packages")
|
||||
end, false)
|
||||
spawn.start("pesde install")
|
||||
spawn.start("lune run install-packages")
|
||||
|
||||
spawn.spawn("rojo sourcemap dev.project.json -o sourcemap.json --watch")
|
||||
spawn.spawn("darklua process --config .darklua.json --watch lib/ dist/", { env = { ROBLOX_DEV = "true" } })
|
||||
|
||||
task.wait(2.5)
|
||||
|
||||
while true do
|
||||
local start_commit = stdio.prompt("confirm", "Start commit? -- `y` to start a commit, `n` to exit the script")
|
||||
if not start_commit then
|
||||
process.exit(0)
|
||||
break
|
||||
end
|
||||
|
||||
local _, check_result = pcall(spawn.start, "lune run check")
|
||||
if not check_result.ok then
|
||||
warn("Check didn't go ok, aborting commit")
|
||||
break
|
||||
end
|
||||
|
||||
local commit_title = stdio.prompt("text", "Commit title -- leave blank to stop committing")
|
||||
if not commit_title or commit_title == "" then
|
||||
print("Stopping commit")
|
||||
continue
|
||||
end
|
||||
local commit_messages = { `-m`, commit_title }
|
||||
|
||||
while true do
|
||||
local commit_message = stdio.prompt("text", "Commit message -- added to the description, leave blank to finish")
|
||||
if not commit_message or commit_message == "" then
|
||||
break
|
||||
end
|
||||
|
||||
table.insert(commit_messages, "-m")
|
||||
table.insert(commit_messages, commit_message)
|
||||
end
|
||||
|
||||
local confirm = stdio.prompt("confirm", "Confirm?")
|
||||
if not confirm then
|
||||
break
|
||||
end
|
||||
|
||||
spawn.start("git add .")
|
||||
process.spawn("git", { "commit", unpack(commit_messages) }, { stdio = "forward" })
|
||||
spawn.start("git push")
|
||||
end
|
||||
|
|
21
.lune/dist.luau
Normal file
21
.lune/dist.luau
Normal file
|
@ -0,0 +1,21 @@
|
|||
--!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
|
67
.lune/download-jecs.luau
Normal file
67
.lune/download-jecs.luau
Normal file
|
@ -0,0 +1,67 @@
|
|||
--!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.luau`)
|
6
.lune/install-packages.luau
Normal file
6
.lune/install-packages.luau
Normal file
|
@ -0,0 +1,6 @@
|
|||
--!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 options process.SpawnOptions?
|
||||
--- @return process.SpawnResult
|
||||
local function start_process(cmd: string, options: process.ExecOptions?): process.ExecResult
|
||||
local function start_process(cmd: string, options: process.SpawnOptions?): process.SpawnResult
|
||||
local arguments = string.split(cmd, " ")
|
||||
local command = arguments[1]
|
||||
table.remove(arguments, 1)
|
||||
|
||||
local opts: process.ExecOptions = options ~= nil and options or {}
|
||||
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward" :: any
|
||||
local opts: process.SpawnOptions = options ~= nil and options or {}
|
||||
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward"
|
||||
|
||||
return process.exec(command, arguments, opts)
|
||||
return process.spawn(command, arguments, opts)
|
||||
end
|
||||
|
||||
--- `task.spawn` a process with the given command and options
|
||||
|
@ -27,7 +27,7 @@ end
|
|||
--- @param cmd string
|
||||
--- @param options process.SpawnOptions?
|
||||
--- @return process.SpawnResult
|
||||
local function spawn_process(cmd: string, options: process.ExecOptions?)
|
||||
local function spawn_process(cmd: string, options: process.SpawnOptions?)
|
||||
task.spawn(start_process, cmd, options)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,47 +1,52 @@
|
|||
// 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#settings-files
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||
{
|
||||
"lsp": {
|
||||
"luau-lsp": {
|
||||
"settings": {
|
||||
"luau-lsp": {
|
||||
"diagnostics": {
|
||||
"workspace": false
|
||||
},
|
||||
"completion": {
|
||||
"imports": {
|
||||
"enabled": true,
|
||||
"suggestServices": true,
|
||||
"suggestRequires": true,
|
||||
"stringRequires": {
|
||||
"enabled": true
|
||||
}
|
||||
"suggestRequires": false
|
||||
}
|
||||
},
|
||||
"sourcemap": {
|
||||
"rojoProjectFile": "dev.project.json"
|
||||
},
|
||||
"require": {
|
||||
"mode": "relativeToFile"
|
||||
"mode": "relativeToFile",
|
||||
"fileAliases": {
|
||||
"@jecs_utils": "lib",
|
||||
"@testkit": "test/testkit",
|
||||
"@jecs": "Packages/jecs"
|
||||
},
|
||||
"directoryAliases": {
|
||||
"@lune": ".lune/.lune-defs/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ext": {
|
||||
"roblox": {
|
||||
"enabled": false,
|
||||
"security_level": "roblox_script"
|
||||
"enabled": false
|
||||
},
|
||||
"fflags": {
|
||||
"enable_new_solver": true,
|
||||
"override": {
|
||||
"LuauTinyControlFlowAnalysis": "true"
|
||||
},
|
||||
"sync": true,
|
||||
"enable_by_default": false
|
||||
},
|
||||
"binary": {
|
||||
"ignore_system_version": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_types": {
|
||||
"Luau": ["lua"]
|
||||
"languages": {
|
||||
"TypeScript": {
|
||||
"tab_size": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 marked
|
||||
Copyright (c) 2024 Mark-Marks
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
117
README.md
117
README.md
|
@ -1,113 +1,18 @@
|
|||
<p align="center">
|
||||
<img src="assets/hammer-logo.png">
|
||||
</p>
|
||||
|
||||
[](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>
|
||||
# sapphire-utils
|
||||
[](https://github.com/mark-marks/jecs-utils/actions/workflows/ci.yml)
|
||||
[](https://wally.run/package/mark-marks/jecs-utils)
|
||||
[](https://github.com/Mark-Marks/jecs-utils/blob/main/LICENSE)
|
||||
|
||||
A set of utilities for [Jecs](https://github.com/ukendio/jecs)
|
||||
<br/>
|
||||
|
||||
</div>
|
||||
|
||||
## Installation
|
||||
## Features
|
||||
|
||||
Hammer is available on pesde @ `marked/hammer` and Wally @ `mark-marks/hammer`.
|
||||
|
||||
## Usage
|
||||
|
||||
All utilities that require a Jecs world to function are exposed via a constructor pattern.\
|
||||
For instance, to build a `ref`:
|
||||
```luau
|
||||
local ref = hammer.ref(world)
|
||||
```
|
||||
This is the easiest solution for passing a world that doesn't sacrifice readability internally and externally or bind the developer to a Jecs version that hammer is currently using.
|
||||
|
||||
### collect
|
||||
|
||||
A [collect](/lib/utilities/collect.luau) collects all arguments fired through the given signal, and exposes an iterator for them.\
|
||||
Its purpose is to interface with signals in ECS code, which ideally should run every frame in a loop.
|
||||
|
||||
For instance, take Roblox's RemoteEvents:
|
||||
```luau
|
||||
local pings = hammer.collect(events.ping)
|
||||
local function system()
|
||||
for _, player, ping in pings do
|
||||
events.ping:FireClient(player, "pong!")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### command_buffer
|
||||
|
||||
A [command_buffer](/lib/utilities/command_buffer.luau) lets you buffer world commands in order to prevent iterator invalidation.\
|
||||
Iterator invalidation refers to an iterator (e.g. `world:query(Component)`) becoming unusable due to changes in the underlying data.
|
||||
|
||||
To prevent this, command buffers can be used to delay world operations to the end of the current frame:
|
||||
```luau
|
||||
local command_buffer = hammer.command_buffer(world)
|
||||
|
||||
while true do
|
||||
step_systems()
|
||||
command_buffer.flush()
|
||||
end
|
||||
|
||||
-- Inside a system:
|
||||
command_buffer.add(entity, component) -- This runs after all of the systems run; no data changes while things are running
|
||||
```
|
||||
|
||||
### ref
|
||||
|
||||
A [ref](/lib/utilities/ref.luau) allows for storing and getting entities via some form of reference.\
|
||||
This is particularly useful for situations where you reconcile entities into your world from a foreign place, e.g. from across a networking boundary.
|
||||
```luau
|
||||
local ref = hammer.ref(world)
|
||||
|
||||
for id in net.new_entities.iter() do
|
||||
local entity = ref(`foreign-{id}`) -- A new entity that can be tracked via a foreign id
|
||||
end
|
||||
```
|
||||
|
||||
Refs by default create a new entity if the given value doesn't reference any stored one. In case you want to see if a reference exists, you can find one:
|
||||
```luau
|
||||
local entity[: Entity?] = ref.find(`my-key`)
|
||||
```
|
||||
|
||||
Refs can also be deleted. All functions used to a fetch a reference also return a cleanup function:
|
||||
```luau
|
||||
local entity, destroy_reference = ref(`my-key`)
|
||||
destroy_reference() -- `entity` still persists in the world, but `my-key` doesn't refer to it anymore.
|
||||
```
|
||||
|
||||
Refs are automatically cached by world. `ref(world)` will have the same underlying references as `ref(world)`.\
|
||||
In case you need an unique reference store, you can omit the cache via `ref(world, true)`.
|
||||
|
||||
### tracker
|
||||
|
||||
A [tracker](/lib/utilities/tracker) keeps a history of all components passed to it, and how to get to their current state in the least amount of commands.\
|
||||
They're great for replicating world state across a networking barrier, as you're able to easily get diffed snapshots and apply them.
|
||||
```luau
|
||||
local tracker = hammer.tracker(world, ComponentA, ComponentB)
|
||||
|
||||
world:set(entity_a, ComponentA, 50)
|
||||
world:add(entity_b, ComponentB)
|
||||
|
||||
-- Says how to give `entity_a` `ComponentA` with the value of `50` and give `entity_b` `ComponentB`.
|
||||
-- `state()` always tracks from when the tracker was first created.
|
||||
local state = tracker.state()
|
||||
|
||||
-- Same as the above, but now this sets the origin for the next taken snapshot!
|
||||
local snapshot = tracker.snapshot()
|
||||
|
||||
world:remove(entity_b, ComponentB)
|
||||
|
||||
-- This now only says to remove `ComponentB` from `entity_b`.
|
||||
local snapshot_b = tracker.snapshot()
|
||||
```
|
||||
|
||||
Trackers simplify the state internally. Removals remove all prior commands pertaining to the entity and component pair, adds remove all prior removals, etc.
|
||||
|
||||
Trackers are optimized under the hood with lookup tables for arrays, to allow for a constant time operation to check for whether it has a member or not. It can lead to worse memory usage, but makes it faster overall.
|
||||
- [collect](/lib/collect.luau) - Collects all arguments fired through the given signal, and drains the collection on iteration.
|
||||
- [handle](/lib/handle.luau) - Wrap `jecs.World` functions for faster (DX wise) operating on entities
|
||||
- [replicator](/lib/replicator.luau) - Keep track of all entities with the passed components and calculate differences
|
||||
- [ref](/lib/ref.luau) - Reference entities by key
|
||||
- [command_buffer](/lib/command_buffer.luau) - Buffer commands to prevent iterator invalidation
|
||||
- [spawner](/lib/spawner.luau) - Spawn entities with required components
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 37 KiB |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hammer",
|
||||
"name": "jecs-utils",
|
||||
"tree": {
|
||||
"$path": "lib"
|
||||
"$path": "dist"
|
||||
}
|
||||
}
|
||||
|
|
16
dev.project.json
Normal file
16
dev.project.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "dev",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
|
||||
"ReplicatedStorage": {
|
||||
"Packages": {
|
||||
"$className": "Folder",
|
||||
"$path": "Packages",
|
||||
"jecs_utils": {
|
||||
"$path": "lib"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
jecs.luau
13
jecs.luau
|
@ -1,13 +0,0 @@
|
|||
--!strict
|
||||
--!native
|
||||
--!optimize 2
|
||||
-- pesde adds dependencies by including a folder with them in the package
|
||||
-- wally just adds a file for each dependency
|
||||
-- this is here to mitigate that
|
||||
local jecs = require("./luau_packages/jecs")
|
||||
export type Archetype = jecs.Archetype
|
||||
export type Id<T = unknown> = jecs.Id<T>
|
||||
export type Pair<First, Second> = jecs.Pair<First, Second>
|
||||
export type Entity<T = unknown> = jecs.Entity<T>
|
||||
export type World = jecs.World
|
||||
return jecs
|
|
@ -1,5 +1,6 @@
|
|||
--!strict
|
||||
--!optimize 2
|
||||
|
||||
--[[
|
||||
original author by @memorycode
|
||||
|
||||
|
@ -27,12 +28,24 @@ SOFTWARE.
|
|||
--]]
|
||||
|
||||
--- What signals passed to `collect()` should be able to be coerced into
|
||||
export type SignalLike<D, T...> = { connect: Connector<D, T...>, [any]: any } | { Connect: Connector<D, T...>, [any]: any }
|
||||
type Connector<D, T...> = (self: SignalLike<D, T...>, (T...) -> ()) -> D
|
||||
export type signal_like<D, T...> = { connect: confn<D, T...>, [any]: any } | { Connect: confn<D, T...>, [any]: any }
|
||||
type confn<D, T...> = (self: signal_like<D, T...>, (T...) -> ()) -> D
|
||||
|
||||
--- Collects all arguments fired through the given signal, and drains the collection on iteration.\
|
||||
--- Expects signals to have a `Connect` or `connect` ***method***.
|
||||
local function collect<D, T...>(event: SignalLike<D, T...>): (() -> (number, T...), D)
|
||||
--- Expects signals to have a `Connect` ***method***.
|
||||
--- ```luau
|
||||
--- local sig = collect(some_signal)
|
||||
---
|
||||
--- -- Imagine this as an ECS scheduler loop
|
||||
--- while task.wait() do
|
||||
--- for index, arg1 in sig do -- arg1, arg2, etc
|
||||
--- print(arg1)
|
||||
--- end
|
||||
--- end
|
||||
--- ```
|
||||
--- @param event signal<T...>
|
||||
--- @return () -> (number, T...), D -- iterator and disconnector
|
||||
local function collect<D, T...>(event: signal_like<D, T...>): (() -> (number, T...), D)
|
||||
local storage = {}
|
||||
local mt = {}
|
||||
local iter = function()
|
||||
|
@ -40,11 +53,11 @@ local function collect<D, T...>(event: SignalLike<D, T...>): (() -> (number, T..
|
|||
return function(): (number?, T...)
|
||||
if n <= 0 then
|
||||
mt.__iter = nil
|
||||
return nil :: any
|
||||
return nil
|
||||
end
|
||||
|
||||
n -= 1
|
||||
return n + 1, unpack(table.remove(storage, 1) :: any) :: any
|
||||
return n + 1, unpack(table.remove(storage, 1) :: any)
|
||||
end
|
||||
end
|
||||
|
135
lib/command_buffer.luau
Normal file
135
lib/command_buffer.luau
Normal file
|
@ -0,0 +1,135 @@
|
|||
--!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
|
78
lib/handle.luau
Normal file
78
lib/handle.luau
Normal file
|
@ -0,0 +1,78 @@
|
|||
--!strict
|
||||
--!optimize 2
|
||||
local jecs = require("../jecs")
|
||||
type entity<T = nil> = jecs.Entity<T>
|
||||
type id<T = nil> = entity<T> | jecs.Pair
|
||||
|
||||
local world = require("./world").get
|
||||
|
||||
type interface = {
|
||||
__index: interface,
|
||||
|
||||
new: (entity: entity) -> handle,
|
||||
|
||||
--- Checks if the entity has all of the given components
|
||||
has: (self: handle, ...id) -> boolean,
|
||||
--- Retrieves the value of up to 4 components. These values may be nil.
|
||||
get: (<A>(self: handle, id<A>) -> A?)
|
||||
& (<A, B>(self: handle, id<A>, id<B>) -> (A?, B?))
|
||||
& (<A, B, C>(self: handle, id<A>, id<B>, id<C>) -> (A?, B?, C?))
|
||||
& (<A, B, C, D>(self: handle, id<A>, id<B>, id<C>, id<D>) -> (A?, B?, C?, D?)),
|
||||
--- Adds a component to the entity with no value
|
||||
add: <T>(self: handle, id: id<T>) -> handle,
|
||||
--- Assigns a value to a component on the given entity
|
||||
set: <T>(self: handle, id: id<T>, data: T) -> handle,
|
||||
--- Removes a component from the given entity
|
||||
remove: (self: handle, id: id) -> handle,
|
||||
--- Deletes the entity and all its related components and relationships. **Does not** refer to deleting the handle
|
||||
delete: (self: handle) -> (),
|
||||
--- Gets the entitys id
|
||||
id: (self: handle) -> entity,
|
||||
}
|
||||
|
||||
export type handle = typeof(setmetatable({} :: { entity: entity, 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,22 +1,42 @@
|
|||
--!strict
|
||||
--!optimize 2
|
||||
local collect = require("@self/utilities/collect")
|
||||
export type SignalLike<T...> = collect.SignalLike<any, T...>
|
||||
export type VerboseSignalLike<D, T...> = collect.SignalLike<D, T...>
|
||||
local jecs = require("../jecs")
|
||||
|
||||
local ref = require("@self/utilities/ref")
|
||||
export type Ref = ref.Identity
|
||||
local WORLD = require("./world")
|
||||
|
||||
local tracker = require("@self/utilities/tracker")
|
||||
export type Tracker = tracker.Identity
|
||||
export type TrackerCommands = tracker.Commands
|
||||
local collect = require("./collect")
|
||||
export type collect_signal_like<T...> = collect.signal_like<any, T...>
|
||||
export type collect_verbose_signal_like<D, T...> = collect.signal_like<D, T...>
|
||||
|
||||
local command_buffer = require("@self/utilities/command_buffer")
|
||||
export type CommandBuffer = command_buffer.Identity
|
||||
local command_buffer = require("./command_buffer")
|
||||
export type command_buffer = command_buffer.command_buffer
|
||||
|
||||
local handle = require("./handle")
|
||||
export type handle = handle.handle
|
||||
|
||||
local ref = require("./ref")
|
||||
|
||||
local replicator = require("./replicator")
|
||||
export type replicator = replicator.replicator
|
||||
export type changes = replicator.changes
|
||||
|
||||
local spawner = require("./spawner")
|
||||
export type spawner<T...> = spawner.spawner<T...>
|
||||
|
||||
--- Set the world for all utilities.
|
||||
--- Should be called once per context before any utility is used.
|
||||
--- @param world jecs.World
|
||||
local function initialize(world: jecs.World)
|
||||
WORLD.set(world)
|
||||
end
|
||||
|
||||
return {
|
||||
initialize = initialize,
|
||||
|
||||
collect = collect,
|
||||
handle = handle,
|
||||
replicator = replicator,
|
||||
ref = ref,
|
||||
tracker = tracker,
|
||||
command_buffer = command_buffer,
|
||||
spawner = spawner,
|
||||
}
|
||||
|
|
67
lib/ref.luau
Normal file
67
lib/ref.luau
Normal file
|
@ -0,0 +1,67 @@
|
|||
--!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
|
247
lib/replicator.luau
Normal file
247
lib/replicator.luau
Normal file
|
@ -0,0 +1,247 @@
|
|||
--!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
|
49
lib/spawner.luau
Normal file
49
lib/spawner.luau
Normal file
|
@ -0,0 +1,49 @@
|
|||
--!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
|
391
lib/spawner_type.luau
Normal file
391
lib/spawner_type.luau
Normal file
|
@ -0,0 +1,391 @@
|
|||
--!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,140 +0,0 @@
|
|||
--!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
|
|
@ -1,85 +0,0 @@
|
|||
--!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
|
|
@ -1,273 +0,0 @@
|
|||
--!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
|
29
lib/world.luau
Normal file
29
lib/world.luau
Normal file
|
@ -0,0 +1,29 @@
|
|||
--!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,
|
||||
}
|
10
luau_lsp_settings.json
Normal file
10
luau_lsp_settings.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"luau-lsp.fflags.override": {
|
||||
"LuauTinyControlFlowAnalysis": "true"
|
||||
},
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.fileAliases": {
|
||||
"@jecs": "Packages/jecs"
|
||||
},
|
||||
"luau-lsp.platform.type": "roblox"
|
||||
}
|
19
pesde.toml
19
pesde.toml
|
@ -1,18 +1,17 @@
|
|||
name = "marked/hammer"
|
||||
version = "0.2.0"
|
||||
description = "A set of utilities for Jecs"
|
||||
authors = ["marked"]
|
||||
repository = "https://git.devmarked.win/marked/hammer"
|
||||
name = "mark_marks/jecs_utils"
|
||||
version = "0.1.6"
|
||||
description = "A set of utilities for jecs"
|
||||
authors = ["marked/Mark-Marks"]
|
||||
repository = "https://github.com/mark-marks/jecs-utils"
|
||||
license = "MIT"
|
||||
includes = ["lib", "lib/**", "LICENSE", "pesde.toml", "README.md", "jecs.luau"]
|
||||
|
||||
include = ["src", "src/**", "LICENSE", "pesde.toml", "README.md"]
|
||||
|
||||
[target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[indices]
|
||||
default = "https://github.com/pesde-pkg/index"
|
||||
default = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[dependencies]
|
||||
# `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" }
|
||||
jecs = { repo = "https://git.devmarked.win/marked/jecs-pesde", rev = "main" }
|
||||
|
|
14
rokit.toml
14
rokit.toml
|
@ -5,9 +5,11 @@
|
|||
|
||||
[tools]
|
||||
wally = "upliftgames/wally@0.3.2"
|
||||
rojo = "rojo-rbx/rojo@7.5.1"
|
||||
lune = "lune-org/lune@0.9.2"
|
||||
selene = "kampfkarren/selene@0.28.0"
|
||||
luau-lsp = "johnnymorganz/luau-lsp@1.45.0"
|
||||
stylua = "johnnymorganz/stylua@2.1.0"
|
||||
pesde = "daimond113/pesde@0.6.2+registry.0.2.2"
|
||||
rojo = "rojo-rbx/rojo@7.4.4"
|
||||
lune = "lune-org/lune@0.8.8"
|
||||
selene = "kampfkarren/selene@0.27.1"
|
||||
luau-lsp = "johnnymorganz/luau-lsp@1.32.4"
|
||||
stylua = "johnnymorganz/stylua@0.20.0"
|
||||
wally-package-types = "johnnymorganz/wally-package-types@1.3.2"
|
||||
darklua = "seaofvoices/darklua@0.13.1"
|
||||
pesde = "daimond113/pesde@0.5.0-rc.8"
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
std = "selene_definitions"
|
||||
exclude = ["lib/jecs.luau"]
|
||||
|
|
|
@ -3,7 +3,7 @@ line_endings = "Unix"
|
|||
indent_type = "Spaces"
|
||||
indent_width = 4
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Input"
|
||||
call_parentheses = "Always"
|
||||
collapse_simple_statement = "Never"
|
||||
|
||||
[sort_requires]
|
||||
|
|
|
@ -3,19 +3,19 @@
|
|||
-- https://github.com/red-blox/Util/blob/main/libs/Signal/Signal.luau
|
||||
-- adapted to work in pure luau
|
||||
|
||||
type Node<T...> = {
|
||||
next: Node<T...>?,
|
||||
type node<T...> = {
|
||||
next: node<T...>?,
|
||||
callback: (T...) -> (),
|
||||
}
|
||||
|
||||
export type Signal<T...> = {
|
||||
root: Node<T...>?,
|
||||
export type signal<T...> = {
|
||||
root: node<T...>?,
|
||||
|
||||
connect: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
||||
wait: (self: Signal<T...>) -> T...,
|
||||
once: (self: Signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
||||
fire: (self: Signal<T...>, T...) -> (),
|
||||
disconnect_all: (self: Signal<T...>) -> (),
|
||||
connect: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
||||
wait: (self: signal<T...>) -> T...,
|
||||
once: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
|
||||
fire: (self: signal<T...>, T...) -> (),
|
||||
disconnect_all: (self: signal<T...>) -> (),
|
||||
}
|
||||
|
||||
local Signal = {}
|
||||
|
@ -23,7 +23,7 @@ Signal.__index = Signal
|
|||
|
||||
-- Extracted this function from Connect as it results in the closure
|
||||
-- made in Connect using less memory because this function can be static
|
||||
local function disconnect<T...>(self: Signal<T...>, Node: Node<T...>)
|
||||
local function disconnect<T...>(self: signal<T...>, Node: node<T...>)
|
||||
if self.root == Node then
|
||||
self.root = Node.next
|
||||
else
|
||||
|
@ -40,7 +40,7 @@ local function disconnect<T...>(self: Signal<T...>, Node: Node<T...>)
|
|||
end
|
||||
end
|
||||
|
||||
function Signal.connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
|
||||
function Signal.connect<T...>(self: signal<T...>, Callback: (T...) -> ()): () -> ()
|
||||
local node = {
|
||||
next = self.root,
|
||||
callback = Callback,
|
||||
|
@ -53,30 +53,30 @@ function Signal.connect<T...>(self: Signal<T...>, Callback: (T...) -> ()): () ->
|
|||
end
|
||||
end
|
||||
|
||||
function Signal.wait<T...>(self: Signal<T...>): T...
|
||||
function Signal.wait<T...>(self: signal<T...>): T...
|
||||
local Thread = coroutine.running()
|
||||
local Disconnect
|
||||
|
||||
Disconnect = self:connect(function(...)
|
||||
(Disconnect :: any)()
|
||||
Disconnect()
|
||||
coroutine.resume(Thread, ...)
|
||||
end)
|
||||
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
function Signal.once<T...>(self: Signal<T...>, Callback: (T...) -> ()): () -> ()
|
||||
function Signal.once<T...>(self: signal<T...>, Callback: (T...) -> ()): () -> ()
|
||||
local Disconnect
|
||||
|
||||
Disconnect = self:connect(function(...)
|
||||
(Disconnect :: any)()
|
||||
Disconnect()
|
||||
Callback(...)
|
||||
end)
|
||||
|
||||
return Disconnect
|
||||
end
|
||||
|
||||
function Signal.fire<T...>(self: Signal<T...>, ...: T...)
|
||||
function Signal.fire<T...>(self: signal<T...>, ...: T...)
|
||||
local Current = self.root
|
||||
|
||||
while Current do
|
||||
|
@ -85,11 +85,11 @@ function Signal.fire<T...>(self: Signal<T...>, ...: T...)
|
|||
end
|
||||
end
|
||||
|
||||
function Signal.disconnect_all<T...>(self: Signal<T...>)
|
||||
function Signal.disconnect_all<T...>(self: signal<T...>)
|
||||
self.root = nil
|
||||
end
|
||||
|
||||
return function<T...>(): Signal<T...>
|
||||
return function<T...>(): signal<T...>
|
||||
return setmetatable({
|
||||
root = nil,
|
||||
}, Signal) :: any
|
||||
|
|
365
test/tests.luau
365
test/tests.luau
|
@ -1,15 +1,18 @@
|
|||
--!strict
|
||||
-- stylua: ignore start
|
||||
local hammer = require("../lib")
|
||||
local jecs = require("@pkg/jecs")
|
||||
local testkit = require("./testkit")
|
||||
local jecs = require("@jecs")
|
||||
local jecs_utils = require("@jecs_utils")
|
||||
local testkit = require("@testkit")
|
||||
|
||||
type Entity<T = unknown> = jecs.Entity<T>
|
||||
type entity<T = nil> = jecs.Entity<T>
|
||||
|
||||
local collect = hammer.collect
|
||||
local make_tracker = hammer.tracker
|
||||
local make_ref = hammer.ref
|
||||
local make_command_buffer = hammer.command_buffer
|
||||
local collect = jecs_utils.collect
|
||||
local handle = jecs_utils.handle
|
||||
local replicator = jecs_utils.replicator
|
||||
local ref = jecs_utils.ref
|
||||
local ref_search = ref.search
|
||||
local command_buffer = jecs_utils.command_buffer
|
||||
local spawner = jecs_utils.spawner
|
||||
|
||||
local signal = require("./signal")
|
||||
|
||||
|
@ -17,9 +20,9 @@ local BENCH, START = testkit.benchmark()
|
|||
|
||||
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
|
||||
|
||||
TEST("hammer.collect()", function()
|
||||
TEST("jecs_utils.collect()", function()
|
||||
do CASE "collects"
|
||||
local sig: signal.Signal<number> = signal()
|
||||
local sig: signal.signal<number> = signal()
|
||||
local flush = collect(sig)
|
||||
local should = {}
|
||||
|
||||
|
@ -35,152 +38,169 @@ TEST("hammer.collect()", function()
|
|||
end
|
||||
end)
|
||||
|
||||
TEST("hammer.ref()", function()
|
||||
do CASE "set_ref"
|
||||
TEST("jecs_utils.handle()", function()
|
||||
do CASE "has"
|
||||
local world = jecs.World.new()
|
||||
local ref = make_ref(world, true)
|
||||
|
||||
local a = ref(1234)
|
||||
local b = ref(1234)
|
||||
CHECK(a == b)
|
||||
end
|
||||
|
||||
do CASE "find"
|
||||
local world = jecs.World.new()
|
||||
local ref = make_ref(world, true)
|
||||
|
||||
local a = ref(1234)
|
||||
local b = ref.find(1234)
|
||||
CHECK(a == b)
|
||||
end
|
||||
|
||||
do CASE "cleaner"
|
||||
local world = jecs.World.new()
|
||||
local ref = make_ref(world, true)
|
||||
|
||||
local a, clean = ref(1234)
|
||||
clean()
|
||||
local b = ref(1234)
|
||||
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)
|
||||
|
||||
-- TODO! write extensive tests for state operation cleaning
|
||||
TEST("hammer.tracker()", function()
|
||||
do CASE "snapshot"
|
||||
local world = jecs.World.new()
|
||||
local tag = world:entity()
|
||||
local component = world:component() :: Entity<number>
|
||||
|
||||
local entity1 = world:entity()
|
||||
local entity2 = world:entity()
|
||||
|
||||
local tracker = make_tracker(world, component, tag)
|
||||
|
||||
world:add(entity1, tag)
|
||||
world:set(entity2, component, 50)
|
||||
|
||||
local snapshot = tracker.snapshot()
|
||||
CHECK(snapshot ~= nil)
|
||||
assert(snapshot) -- Refinements
|
||||
|
||||
local world2 = jecs.World.new()
|
||||
local component2 = world2:component() :: Entity<number>
|
||||
local tag2 = world2:entity()
|
||||
|
||||
local tracker2 = make_tracker(world2, component2, tag2)
|
||||
|
||||
tracker2.apply(snapshot)
|
||||
|
||||
CHECK(world:has(entity1, tag2))
|
||||
CHECK(world:get(entity2, component2) == 50)
|
||||
end
|
||||
|
||||
do CASE "state"
|
||||
local world = jecs.World.new()
|
||||
local tag = world:entity()
|
||||
local component = world:component() :: Entity<number>
|
||||
|
||||
local entity1 = world:entity()
|
||||
local entity2 = world:entity()
|
||||
|
||||
local tracker = make_tracker(world, component, tag)
|
||||
|
||||
world:add(entity1, tag)
|
||||
world:set(entity2, component, 50)
|
||||
|
||||
local state = tracker.state()
|
||||
CHECK(state ~= nil)
|
||||
|
||||
local world2 = jecs.World.new()
|
||||
local component2 = world2:component() :: Entity<number>
|
||||
local tag2 = world2:entity()
|
||||
|
||||
local tracker2 = make_tracker(world2, component2, tag2)
|
||||
|
||||
tracker2.apply(state)
|
||||
|
||||
CHECK(world:has(entity1, tag2))
|
||||
CHECK(world:get(entity2, component2) == 50)
|
||||
end
|
||||
|
||||
do CASE "simplifying"
|
||||
local world = jecs.World.new()
|
||||
local component = world:component() :: Entity<number>
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local entity = world:entity()
|
||||
local tracker = make_tracker(world, component)
|
||||
local tag = world:entity()
|
||||
|
||||
world:add(entity, component)
|
||||
do
|
||||
local state = tracker.state()
|
||||
CHECK(table.find(state.added[component :: any], 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)
|
||||
do
|
||||
local state = tracker.state()
|
||||
CHECK(not table.find(state.added[component :: any], entity))
|
||||
CHECK(state.set[component :: any][entity :: any] == 50)
|
||||
CHECK(handle(entity):get(component) == 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))
|
||||
|
||||
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
|
||||
world:add(entity, component)
|
||||
do
|
||||
local state = tracker.state()
|
||||
CHECK(not table.find(state.removed[component :: any], entity))
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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("hammer.command_buffer()", function()
|
||||
TEST("jecs_utils.ref()", function()
|
||||
do CASE "set_ref"
|
||||
local world = jecs.World.new()
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local a: number = ref(1234):id()
|
||||
local b: number = ref(1234):id()
|
||||
CHECK(a == b)
|
||||
end
|
||||
|
||||
do CASE "search"
|
||||
local world = jecs.World.new()
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local a: number = ref(1234):id()
|
||||
local b = ref_search(1234)
|
||||
assert(b) -- give me the type refinements...
|
||||
CHECK(a == b:id() :: number)
|
||||
end
|
||||
|
||||
do CASE "clearer"
|
||||
local world = jecs.World.new()
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local a, a_clear = ref(1234);
|
||||
(a_clear :: any)()
|
||||
local b = ref(1234)
|
||||
CHECK(b:id() :: number ~= a:id() :: number)
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("jecs_utils.replicator()", function()
|
||||
do CASE "propagates difference"
|
||||
local world = jecs.World.new()
|
||||
local tag = world:entity()
|
||||
local component: jecs.Entity<number> = world:component()
|
||||
|
||||
local entity1 = world:entity()
|
||||
local entity2 = world:entity()
|
||||
|
||||
jecs_utils.initialize(world)
|
||||
local rep = replicator(component, tag)
|
||||
|
||||
world:add(entity1, tag)
|
||||
world:set(entity2, component, 50)
|
||||
|
||||
local difference: jecs_utils.changes = rep.calculate_difference() :: any
|
||||
CHECK(difference ~= nil)
|
||||
|
||||
local world2 = jecs.World.new()
|
||||
local component2: jecs.Entity<number> = world2:component()
|
||||
local tag2 = world2:entity()
|
||||
|
||||
jecs_utils.initialize(world2)
|
||||
local rep2 = replicator(component2, tag2)
|
||||
|
||||
rep2.apply_difference(difference)
|
||||
|
||||
CHECK(ref(`replicated-{entity1}`):has(tag2))
|
||||
CHECK(ref(`replicated-{entity2}`):get(component2) == 50)
|
||||
end
|
||||
|
||||
do CASE "propagates full data"
|
||||
local world = jecs.World.new()
|
||||
local tag = world:entity()
|
||||
local component: jecs.Entity<number> = world:component()
|
||||
|
||||
local entity1 = world:entity()
|
||||
local entity2 = world:entity()
|
||||
|
||||
jecs_utils.initialize(world)
|
||||
local rep = replicator(component, tag)
|
||||
|
||||
world:add(entity1, tag)
|
||||
world:set(entity2, component, 50)
|
||||
|
||||
local full_data = rep.get_full_data()
|
||||
CHECK(full_data ~= nil)
|
||||
|
||||
local world2 = jecs.World.new()
|
||||
local component2: jecs.Entity<number> = world2:component()
|
||||
local tag2 = world2:entity()
|
||||
|
||||
jecs_utils.initialize(world2)
|
||||
local rep2 = replicator(component2, tag2)
|
||||
|
||||
rep2.apply_difference(full_data)
|
||||
|
||||
CHECK(ref(`replicated-{entity1}`):has(tag2))
|
||||
CHECK(ref(`replicated-{entity2}`):get(component2) == 50)
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("jecs_utils.command_buffer", function()
|
||||
do CASE "add"
|
||||
local world = jecs.World.new()
|
||||
local command_buffer = make_command_buffer(world)
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local tag = world:entity()
|
||||
local entity = world:entity()
|
||||
|
@ -195,7 +215,7 @@ TEST("hammer.command_buffer()", function()
|
|||
|
||||
do CASE "set"
|
||||
local world = jecs.World.new()
|
||||
local command_buffer = make_command_buffer(world)
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local component = world:component()
|
||||
local entity = world:entity()
|
||||
|
@ -210,7 +230,7 @@ TEST("hammer.command_buffer()", function()
|
|||
|
||||
do CASE "remove"
|
||||
local world = jecs.World.new()
|
||||
local command_buffer = make_command_buffer(world)
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local component = world:component()
|
||||
local entity = world:entity()
|
||||
|
@ -226,7 +246,7 @@ TEST("hammer.command_buffer()", function()
|
|||
|
||||
do CASE "delete"
|
||||
local world = jecs.World.new()
|
||||
local command_buffer = make_command_buffer(world)
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local entity = world:entity()
|
||||
command_buffer.delete(entity)
|
||||
|
@ -235,32 +255,51 @@ TEST("hammer.command_buffer()", function()
|
|||
|
||||
CHECK(not world:contains(entity))
|
||||
end
|
||||
end)
|
||||
|
||||
do CASE "peek"
|
||||
TEST("jecs_utils.spawner()", function()
|
||||
do CASE "spawn"
|
||||
local world = jecs.World.new()
|
||||
local command_buffer = make_command_buffer(world)
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local tag1 = world:entity()
|
||||
local entity1 = world:entity()
|
||||
command_buffer.add(entity1, tag1)
|
||||
local c1: entity<number> = world:component()
|
||||
local c2: entity<string> = world:component()
|
||||
local c3: entity<{}> = world:component()
|
||||
|
||||
local component1 = world:component()
|
||||
local entity2 = world:entity()
|
||||
command_buffer.set(entity2, component1, 50)
|
||||
local t1 = world:entity()
|
||||
|
||||
local tag2 = world:component()
|
||||
local entity3 = world:entity()
|
||||
command_buffer.remove(entity3, tag2)
|
||||
local entity_spawner = spawner(c1, c2, c3, t1)
|
||||
|
||||
local entity4 = world:component()
|
||||
command_buffer.delete(entity4)
|
||||
local tbl = {}
|
||||
|
||||
local commands = command_buffer.peek()
|
||||
CHECK(table.find(commands.add[tag1], entity1))
|
||||
CHECK(commands.set[component1][entity2] == 50)
|
||||
CHECK(table.find(commands.remove[tag2], entity3))
|
||||
CHECK(table.find(commands.delete, entity4))
|
||||
CHECK(commands.deletion_lookup[entity4] == true)
|
||||
local idx = entity_spawner.spawn(1234, "abcdef", tbl)
|
||||
CHECK(world:contains(idx))
|
||||
CHECK(world:get(idx, c1) == 1234)
|
||||
CHECK(world:get(idx, c2) == "abcdef")
|
||||
CHECK(world:get(idx, c3) == tbl)
|
||||
CHECK(world:has(idx, t1))
|
||||
end
|
||||
|
||||
do CASE "spawn_with_handle"
|
||||
local world = jecs.World.new()
|
||||
jecs_utils.initialize(world)
|
||||
|
||||
local c1: entity<number> = world:component()
|
||||
local c2: entity<string> = world:component()
|
||||
local c3: entity<{}> = world:component()
|
||||
|
||||
local t1 = world:entity()
|
||||
|
||||
local entity_spawner = spawner(c1, c2, c3, t1)
|
||||
|
||||
local tbl = {}
|
||||
|
||||
local ent = entity_spawner.spawn_with_handle(1234, "abcdef", tbl)
|
||||
CHECK(world:contains(ent:id()))
|
||||
CHECK(ent:get(c1) == 1234)
|
||||
CHECK(ent:get(c2) == "abcdef")
|
||||
CHECK(ent:get(c3) == tbl)
|
||||
CHECK(ent:has(t1))
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
10
wally.toml
10
wally.toml
|
@ -1,18 +1,18 @@
|
|||
[package]
|
||||
name = "mark-marks/hammer"
|
||||
version = "0.2.0"
|
||||
name = "mark-marks/jecs-utils"
|
||||
version = "0.1.6"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
license = "MIT"
|
||||
exclude = ["**"]
|
||||
include = [
|
||||
"default.project.json",
|
||||
"lib",
|
||||
"lib/**",
|
||||
"dist",
|
||||
"dist/**",
|
||||
"LICENSE",
|
||||
"wally.toml",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
jecs = "ukendio/jecs@0.5.5"
|
||||
jecs = "ukendio/jecs@0.3.2"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue