From 2a6907434a040b89074354ef5de86099dfa967bc Mon Sep 17 00:00:00 2001 From: marked Date: Wed, 7 May 2025 00:37:24 +0200 Subject: [PATCH] Cleanup & refactor --- .darklua.json | 17 - .forgejo/workflows/ci.yml | 98 ++++ {.github => .forgejo}/workflows/release.yml | 41 +- .github/workflows/ci.yml | 90 ---- .luaurc | 4 +- .lune/.lune-defs/datetime.luau | 408 ---------------- .lune/.lune-defs/fs.luau | 289 ----------- .lune/.lune-defs/luau.luau | 123 ----- .lune/.lune-defs/net.luau | 321 ------------- .lune/.lune-defs/process.luau | 182 ------- .lune/.lune-defs/regex.luau | 218 --------- .lune/.lune-defs/roblox.luau | 507 -------------------- .lune/.lune-defs/serde.luau | 200 -------- .lune/.lune-defs/stdio.luau | 161 ------- .lune/.lune-defs/task.luau | 99 ---- .lune/analyze.luau | 8 - .lune/build.luau | 7 +- .lune/check.luau | 4 +- .lune/dev.luau | 57 +-- .lune/dist.luau | 21 - .lune/download-jecs.luau | 69 --- .lune/install-packages.luau | 6 - .lune/util/spawn.luau | 10 +- .zed/settings.json | 29 +- LICENSE | 2 +- README.md | 117 ++++- assets/hammer-logo.png | Bin 0 -> 37746 bytes default.project.json | 4 +- dev.project.json | 19 - jecs.luau | 4 +- lib/command_buffer.luau | 135 ------ lib/handle.luau | 78 --- lib/init.luau | 42 +- lib/ref.luau | 67 --- lib/replicator.luau | 247 ---------- lib/spawner.luau | 49 -- lib/spawner_type.luau | 391 --------------- lib/{ => utilities}/collect.luau | 25 +- lib/utilities/command_buffer.luau | 140 ++++++ lib/utilities/ref.luau | 85 ++++ lib/utilities/tracker.luau | 273 +++++++++++ lib/world.luau | 29 -- luau_lsp_settings.json | 10 - pesde.toml | 17 +- rokit.toml | 14 +- selene.toml | 1 - stylua.toml | 2 +- test/signal.luau | 36 +- test/tests.luau | 281 +++++------ wally.toml | 10 +- 50 files changed, 937 insertions(+), 4110 deletions(-) delete mode 100644 .darklua.json create mode 100644 .forgejo/workflows/ci.yml rename {.github => .forgejo}/workflows/release.yml (57%) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .lune/.lune-defs/datetime.luau delete mode 100644 .lune/.lune-defs/fs.luau delete mode 100644 .lune/.lune-defs/luau.luau delete mode 100644 .lune/.lune-defs/net.luau delete mode 100644 .lune/.lune-defs/process.luau delete mode 100644 .lune/.lune-defs/regex.luau delete mode 100644 .lune/.lune-defs/roblox.luau delete mode 100644 .lune/.lune-defs/serde.luau delete mode 100644 .lune/.lune-defs/stdio.luau delete mode 100644 .lune/.lune-defs/task.luau delete mode 100644 .lune/analyze.luau delete mode 100644 .lune/dist.luau delete mode 100644 .lune/download-jecs.luau delete mode 100644 .lune/install-packages.luau create mode 100644 assets/hammer-logo.png delete mode 100644 dev.project.json delete mode 100644 lib/command_buffer.luau delete mode 100644 lib/handle.luau delete mode 100644 lib/ref.luau delete mode 100644 lib/replicator.luau delete mode 100644 lib/spawner.luau delete mode 100644 lib/spawner_type.luau rename lib/{ => utilities}/collect.luau (74%) create mode 100644 lib/utilities/command_buffer.luau create mode 100644 lib/utilities/ref.luau create mode 100644 lib/utilities/tracker.luau delete mode 100644 lib/world.luau delete mode 100644 luau_lsp_settings.json diff --git a/.darklua.json b/.darklua.json deleted file mode 100644 index 970498b..0000000 --- a/.darklua.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "process": [ - { - "rule": "convert_require", - "current": { - "name": "path", - "sources": { - "@jecs": "Packages/jecs" - } - }, - "target": { - "name": "roblox", - "rojo_sourcemap": "sourcemap.json" - } - } - ] -} diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..4a40166 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,98 @@ +name: Continous Integration + +on: + push: + pull_request: + branches: + - main + +jobs: + build: + name: Build + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 + steps: + - name: Checkout Project + uses: actions/checkout@v4 + + - name: Install Rokit + uses: https://github.com/CompeyDev/setup-rokit@v0.1.2 + with: + token: ${{ secrets.githubtoken }} + + - name: Build + run: | + lune run build + + - name: Upload Build Artifact + uses: https://git.devmarked.win/actions/upload-artifact@v4 + with: + name: build + path: hammer.rbxm + + lint: + name: Lint + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Rokit + uses: https://github.com/CompeyDev/setup-rokit@v0.1.2 + with: + token: ${{ secrets.githubtoken }} + + - name: Lint + run: | + selene lib/ + + style: + name: Styling + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Check code style + uses: https://github.com/JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.githubtoken }} + version: v2.1.0 + args: --check lib/ + + test: + name: Unit Testing + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Luau + uses: https://github.com/EncodedVenom/install-luau@v4 + + - name: Install Rokit + uses: https://github.com/CompeyDev/setup-rokit@v0.1.2 + with: + token: ${{ secrets.githubtoken }} + + - name: Install Packages + run: | + pesde install + + - name: Run Unit Tests + run: | + output=$(luau test/tests.luau) + echo "$output" + if [[ "$output" == *"0 fails"* ]]; then + echo "Unit Tests Passed" + else + echo "Error: One or More Unit Tests Failed." + exit 1 + fi diff --git a/.github/workflows/release.yml b/.forgejo/workflows/release.yml similarity index 57% rename from .github/workflows/release.yml rename to .forgejo/workflows/release.yml index 55f7447..0b3d032 100644 --- a/.github/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -7,28 +7,34 @@ on: jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 steps: - name: Checkout Project uses: actions/checkout@v4 - name: Install Rokit - uses: CompeyDev/setup-rokit@v0.1.2 + uses: https://github.com/CompeyDev/setup-rokit@v0.1.2 + with: + token: ${{ secrets.githubtoken }} - name: Build run: | lune run build - name: Upload Build Artifact - uses: actions/upload-artifact@v3 + uses: https://git.devmarked.win/actions/upload-artifact@v4 with: name: build - path: build.rbxm + path: hammer.rbxm release: name: Release needs: [build] - runs-on: ubuntu-latest + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 permissions: contents: write steps: @@ -36,35 +42,32 @@ jobs: uses: actions/checkout@v4 - name: Download Build - uses: actions/download-artifact@v3 + uses: https://git.devmarked.win/actions/download-artifact@v4 with: name: build path: build - - name: Rename Build - run: mv build/build.rbxm jecs_utils.rbxm - - name: Create Release - uses: softprops/action-gh-release@v1 + uses: actions/forgejo-release@v2 with: - name: Jecs Utils ${{ github.ref_name }} - files: | - jecs_utils.rbxm + direction: upload + title: Hammer ${{ github.ref_name }} + release-dir: build publish: name: Publish needs: [release] - runs-on: ubuntu-latest + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:act-24.04 steps: - name: Checkout Project uses: actions/checkout@v4 - name: Install Rokit - uses: CompeyDev/setup-rokit@v0.1.2 - - - name: Prepare for Distribution - run: | - lune run dist + uses: https://github.com/CompeyDev/setup-rokit@v0.1.2 + with: + token: ${{ secrets.githubtoken }} - name: Wally Login run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ec2b079..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Continous Integration - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install Rokit - uses: CompeyDev/setup-rokit@v0.1.2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Analyze - run: lune run analyze - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install Rokit - uses: CompeyDev/setup-rokit@v0.1.2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Lint - run: | - selene lib/ - - style: - name: Styling - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Check code style - uses: JohnnyMorganz/stylua-action@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - version: v0.20.0 - args: --check lib/ - - test: - name: Unit Testing - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install Luau - uses: encodedvenom/install-luau@v2.1 - - - name: Install Rokit - uses: CompeyDev/setup-rokit@v0.1.2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Packages - run: | - pesde install - - # Not needed anymore thanks to pesde 🥳 - #- name: Download Jecs - # run: | - # lune run download-jecs ${{ secrets.GITHUB_TOKEN }} - - - name: Run Unit Tests - run: | - output=$(luau test/tests.luau) - echo "$output" - if [[ "$output" == *"0 fails"* ]]; then - echo "Unit Tests Passed" - else - echo "Error: One or More Unit Tests Failed." - exit 1 - fi diff --git a/.luaurc b/.luaurc index 1525a3d..b88f9c7 100644 --- a/.luaurc +++ b/.luaurc @@ -1,7 +1,7 @@ { "languageMode": "strict", "aliases": { - "lib": "lib", - "pkg": "luau_packages" + "pkg": "luau_packages", + "lune": "~/.lune/.typedefs/0.9.2" } } diff --git a/.lune/.lune-defs/datetime.luau b/.lune/.lune-defs/datetime.luau deleted file mode 100644 index 8992dab..0000000 --- a/.lune/.lune-defs/datetime.luau +++ /dev/null @@ -1,408 +0,0 @@ ---[[ - NOTE: We export a couple different DateTimeValues types below to ensure - that types are completely accurate, for method args milliseconds will - always be optional, but for return values millis are always included - - If we figure out some better strategy here where we can - export just a single type while maintaining accuracy we - can change to that in a future breaking semver release -]] - -type OptionalMillisecond = { - millisecond: number?, -} - -type Millisecond = { - millisecond: number, -} - ---[=[ - @interface Locale - @within DateTime - - Enum type representing supported DateTime locales. - - Currently supported locales are: - - - `en` - English - - `de` - German - - `es` - Spanish - - `fr` - French - - `it` - Italian - - `ja` - Japanese - - `pl` - Polish - - `pt-br` - Brazilian Portuguese - - `pt` - Portuguese - - `tr` - Turkish -]=] -export type Locale = "en" | "de" | "es" | "fr" | "it" | "ja" | "pl" | "pt-br" | "pt" | "tr" - ---[=[ - @interface DateTimeValues - @within DateTime - - Individual date & time values, representing the primitives that make up a `DateTime`. - - This is a dictionary that will contain the following values: - - - `year` - Year(s), in the range 1400 -> 9999 - - `month` - Month(s), in the range 1 -> 12 - - `day` - Day(s), in the range 1 -> 31 - - `hour` - Hour(s), in the range 0 -> 23 - - `minute` - Minute(s), in the range 0 -> 59 - - `second` - Second(s), in the range 0 -> 60, where 60 is a leap second - - An additional `millisecond` value may also be included, - and should be within the range `0 -> 999`, but is optional. - - However, any method returning this type should be guaranteed - to include milliseconds - see individual methods to verify. -]=] -export type DateTimeValues = { - year: number, - month: number, - day: number, - hour: number, - minute: number, - second: number, -} - ---[=[ - @interface DateTimeValueArguments - @within DateTime - - Alias for `DateTimeValues` with an optional `millisecond` value. - - Refer to the `DateTimeValues` documentation for additional information. -]=] -export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond - ---[=[ - @interface DateTimeValueReturns - @within DateTime - - Alias for `DateTimeValues` with a mandatory `millisecond` value. - - Refer to the `DateTimeValues` documentation for additional information. -]=] -export type DateTimeValueReturns = DateTimeValues & Millisecond - -local DateTime = { - --- Number of seconds passed since the UNIX epoch. - unixTimestamp = (nil :: any) :: number, - --- Number of milliseconds passed since the UNIX epoch. - unixTimestampMillis = (nil :: any) :: number, -} - ---[=[ - @within DateTime - @tag Method - - Formats this `DateTime` using the given `formatString` and `locale`, as local time. - - The given `formatString` is parsed using a `strftime`/`strptime`-inspired - date and time formatting syntax, allowing tokens such as the following: - - | Token | Example | Description | - |-------|----------|---------------| - | `%Y` | `1998` | Year number | - | `%m` | `04` | Month number | - | `%d` | `29` | Day number | - | `%A` | `Monday` | Weekday name | - | `%M` | `59` | Minute number | - | `%S` | `10` | Second number | - - For a full reference of all available tokens, see the - [chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). - - If not provided, `formatString` and `locale` will default - to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively. - - @param formatString -- A string containing formatting tokens - @param locale -- The locale the time should be formatted in - @return string -- The formatting string -]=] -function DateTime.formatLocalTime(self: DateTime, formatString: string?, locale: Locale?): string - return nil :: any -end - ---[=[ - @within DateTime - @tag Method - - Formats this `DateTime` using the given `formatString` and `locale`, as UTC (universal) time. - - The given `formatString` is parsed using a `strftime`/`strptime`-inspired - date and time formatting syntax, allowing tokens such as the following: - - | Token | Example | Description | - |-------|----------|---------------| - | `%Y` | `1998` | Year number | - | `%m` | `04` | Month number | - | `%d` | `29` | Day number | - | `%A` | `Monday` | Weekday name | - | `%M` | `59` | Minute number | - | `%S` | `10` | Second number | - - For a full reference of all available tokens, see the - [chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). - - If not provided, `formatString` and `locale` will default - to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively. - - @param formatString -- A string containing formatting tokens - @param locale -- The locale the time should be formatted in - @return string -- The formatting string -]=] -function DateTime.formatUniversalTime( - self: DateTime, - formatString: string?, - locale: Locale? -): string - return nil :: any -end - ---[=[ - @within DateTime - @tag Method - - Formats this `DateTime` as an ISO 8601 date-time string. - - Some examples of ISO 8601 date-time strings are: - - - `2020-02-22T18:12:08Z` - - `2000-01-31T12:34:56+05:00` - - `1970-01-01T00:00:00.055Z` - - @return string -- The ISO 8601 formatted string -]=] -function DateTime.toIsoDate(self: DateTime): string - return nil :: any -end - ---[=[ - @within DateTime - @tag Method - - Extracts separated local date & time values from this `DateTime`. - - The returned table contains the following values: - - | Key | Type | Range | - |---------------|----------|----------------| - | `year` | `number` | `1400 -> 9999` | - | `month` | `number` | `1 -> 12` | - | `day` | `number` | `1 -> 31` | - | `hour` | `number` | `0 -> 23` | - | `minute` | `number` | `0 -> 59` | - | `second` | `number` | `0 -> 60` | - | `millisecond` | `number` | `0 -> 999` | - - @return DateTimeValueReturns -- A table of DateTime values -]=] -function DateTime.toLocalTime(self: DateTime): DateTimeValueReturns - return nil :: any -end - ---[=[ - @within DateTime - @tag Method - - Extracts separated UTC (universal) date & time values from this `DateTime`. - - The returned table contains the following values: - - | Key | Type | Range | - |---------------|----------|----------------| - | `year` | `number` | `1400 -> 9999` | - | `month` | `number` | `1 -> 12` | - | `day` | `number` | `1 -> 31` | - | `hour` | `number` | `0 -> 23` | - | `minute` | `number` | `0 -> 59` | - | `second` | `number` | `0 -> 60` | - | `millisecond` | `number` | `0 -> 999` | - - @return DateTimeValueReturns -- A table of DateTime values -]=] -function DateTime.toUniversalTime(self: DateTime): DateTimeValueReturns - return nil :: any -end - -export type DateTime = typeof(DateTime) - ---[=[ - @class DateTime - - Built-in library for date & time - - ### Example usage - - ```lua - local DateTime = require("@lune/datetime") - - -- Creates a DateTime for the current exact moment in time - local now = DateTime.now() - - -- Formats the current moment in time as an ISO 8601 string - print(now:toIsoDate()) - - -- Formats the current moment in time, using the local - -- time, the French locale, and the specified time string - print(now:formatLocalTime("%A, %d %B %Y", "fr")) - - -- Returns a specific moment in time as a DateTime instance - local someDayInTheFuture = DateTime.fromLocalTime({ - year = 3033, - month = 8, - day = 26, - hour = 16, - minute = 56, - second = 28, - millisecond = 892, - }) - - -- Extracts the current local date & time as separate values (same values as above table) - print(now:toLocalTime()) - - -- Returns a DateTime instance from a given float, where the whole - -- denotes the seconds and the fraction denotes the milliseconds - -- Note that the fraction for millis here is completely optional - DateTime.fromUnixTimestamp(871978212313.321) - - -- Extracts the current universal (UTC) date & time as separate values - print(now:toUniversalTime()) - ``` -]=] -local dateTime = {} - ---[=[ - @within DateTime - @tag Constructor - - Returns a `DateTime` representing the current moment in time. - - @return DateTime -- The new DateTime object -]=] -function dateTime.now(): DateTime - return nil :: any -end - ---[=[ - @within DateTime - @tag Constructor - - Creates a new `DateTime` from the given UNIX timestamp. - - This timestamp may contain both a whole and fractional part - - where the fractional part denotes milliseconds / nanoseconds. - - Example usage of fractions: - - - `DateTime.fromUnixTimestamp(123456789.001)` - one millisecond - - `DateTime.fromUnixTimestamp(123456789.000000001)` - one nanosecond - - Note that the fractional part has limited precision down to exactly - one nanosecond, any fraction that is more precise will get truncated. - - @param unixTimestamp -- Seconds passed since the UNIX epoch - @return DateTime -- The new DateTime object -]=] -function dateTime.fromUnixTimestamp(unixTimestamp: number): DateTime - return nil :: any -end - ---[=[ - @within DateTime - @tag Constructor - - Creates a new `DateTime` from the given date & time values table, in universal (UTC) time. - - The given table must contain the following values: - - | Key | Type | Range | - |----------|----------|----------------| - | `year` | `number` | `1400 -> 9999` | - | `month` | `number` | `1 -> 12` | - | `day` | `number` | `1 -> 31` | - | `hour` | `number` | `0 -> 23` | - | `minute` | `number` | `0 -> 59` | - | `second` | `number` | `0 -> 60` | - - An additional `millisecond` value may also be included, - and should be within the range `0 -> 999`, but is optional. - - Any non-integer values in the given table will be rounded down. - - ### Errors - - This constructor is fallible and may throw an error in the following situations: - - - Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year. - - @param values -- Table containing date & time values - @return DateTime -- The new DateTime object -]=] -function dateTime.fromUniversalTime(values: DateTimeValueArguments): DateTime - return nil :: any -end - ---[=[ - @within DateTime - @tag Constructor - - Creates a new `DateTime` from the given date & time values table, in local time. - - The given table must contain the following values: - - | Key | Type | Range | - |----------|----------|----------------| - | `year` | `number` | `1400 -> 9999` | - | `month` | `number` | `1 -> 12` | - | `day` | `number` | `1 -> 31` | - | `hour` | `number` | `0 -> 23` | - | `minute` | `number` | `0 -> 59` | - | `second` | `number` | `0 -> 60` | - - An additional `millisecond` value may also be included, - and should be within the range `0 -> 999`, but is optional. - - Any non-integer values in the given table will be rounded down. - - ### Errors - - This constructor is fallible and may throw an error in the following situations: - - - Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year. - - @param values -- Table containing date & time values - @return DateTime -- The new DateTime object -]=] -function dateTime.fromLocalTime(values: DateTimeValueArguments): DateTime - return nil :: any -end - ---[=[ - @within DateTime - @tag Constructor - - Creates a new `DateTime` from an ISO 8601 date-time string. - - ### Errors - - This constructor is fallible and may throw an error if the given - string does not strictly follow the ISO 8601 date-time string format. - - Some examples of valid ISO 8601 date-time strings are: - - - `2020-02-22T18:12:08Z` - - `2000-01-31T12:34:56+05:00` - - `1970-01-01T00:00:00.055Z` - - @param isoDate -- An ISO 8601 formatted string - @return DateTime -- The new DateTime object -]=] -function dateTime.fromIsoDate(isoDate: string): DateTime - return nil :: any -end - -return dateTime diff --git a/.lune/.lune-defs/fs.luau b/.lune/.lune-defs/fs.luau deleted file mode 100644 index 823f6f7..0000000 --- a/.lune/.lune-defs/fs.luau +++ /dev/null @@ -1,289 +0,0 @@ -local DateTime = require("./datetime") -type DateTime = DateTime.DateTime - -export type MetadataKind = "file" | "dir" | "symlink" - ---[=[ - @interface MetadataPermissions - @within FS - - Permissions for the given file or directory. - - This is a dictionary that will contain the following values: - - * `readOnly` - If the target path is read-only or not -]=] -export type MetadataPermissions = { - readOnly: boolean, -} - --- FIXME: We lose doc comments here below in Metadata because of the union type - ---[=[ - @interface Metadata - @within FS - - Metadata for the given file or directory. - - This is a dictionary that will contain the following values: - - * `kind` - If the target path is a `file`, `dir` or `symlink` - * `exists` - If the target path exists - * `createdAt` - The timestamp represented as a `DateTime` object at which the file or directory was created - * `modifiedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last modified - * `accessedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last accessed - * `permissions` - Current permissions for the file or directory - - Note that timestamps are relative to the unix epoch, and - may not be accurate if the system clock is not accurate. -]=] -export type Metadata = { - kind: MetadataKind, - exists: true, - createdAt: DateTime, - modifiedAt: DateTime, - accessedAt: DateTime, - permissions: MetadataPermissions, -} | { - kind: nil, - exists: false, - createdAt: nil, - modifiedAt: nil, - accessedAt: nil, - permissions: nil, -} - ---[=[ - @interface WriteOptions - @within FS - - Options for filesystem APIs what write to files and/or directories. - - This is a dictionary that may contain one or more of the following values: - - * `overwrite` - If the target path should be overwritten or not, in the case that it already exists -]=] -export type WriteOptions = { - overwrite: boolean?, -} - ---[=[ - @class FS - - Built-in library for filesystem access - - ### Example usage - - ```lua - local fs = require("@lune/fs") - - -- Reading a file - local myTextFile: string = fs.readFile("myFileName.txt") - - -- Reading entries (files & dirs) in a directory - for _, entryName in fs.readDir("myDirName") do - if fs.isFile("myDirName/" .. entryName) then - print("Found file " .. entryName) - elseif fs.isDir("myDirName/" .. entryName) then - print("Found subdirectory " .. entryName) - end - end - ``` -]=] -local fs = {} - ---[=[ - @within FS - @tag must_use - - Reads a file at `path`. - - An error will be thrown in the following situations: - - * `path` does not point to an existing file. - * The current process lacks permissions to read the file. - * Some other I/O error occurred. - - @param path The path to the file to read - @return The contents of the file -]=] -function fs.readFile(path: string): string - return nil :: any -end - ---[=[ - @within FS - @tag must_use - - Reads entries in a directory at `path`. - - An error will be thrown in the following situations: - - * `path` does not point to an existing directory. - * The current process lacks permissions to read the contents of the directory. - * Some other I/O error occurred. - - @param path The directory path to search in - @return A list of files & directories found -]=] -function fs.readDir(path: string): { string } - return {} -end - ---[=[ - @within FS - - Writes to a file at `path`. - - An error will be thrown in the following situations: - - * The file's parent directory does not exist. - * The current process lacks permissions to write to the file. - * Some other I/O error occurred. - - @param path The path of the file - @param contents The contents of the file -]=] -function fs.writeFile(path: string, contents: buffer | string) end - ---[=[ - @within FS - - Creates a directory and its parent directories if they are missing. - - An error will be thrown in the following situations: - - * `path` already points to an existing file or directory. - * The current process lacks permissions to create the directory or its missing parents. - * Some other I/O error occurred. - - @param path The directory to create -]=] -function fs.writeDir(path: string) end - ---[=[ - @within FS - - Removes a file. - - An error will be thrown in the following situations: - - * `path` does not point to an existing file. - * The current process lacks permissions to remove the file. - * Some other I/O error occurred. - - @param path The file to remove -]=] -function fs.removeFile(path: string) end - ---[=[ - @within FS - - Removes a directory and all of its contents. - - An error will be thrown in the following situations: - - * `path` is not an existing and empty directory. - * The current process lacks permissions to remove the directory. - * Some other I/O error occurred. - - @param path The directory to remove -]=] -function fs.removeDir(path: string) end - ---[=[ - @within FS - @tag must_use - - Gets metadata for the given path. - - An error will be thrown in the following situations: - - * The current process lacks permissions to read at `path`. - * Some other I/O error occurred. - - @param path The path to get metadata for - @return Metadata for the path -]=] -function fs.metadata(path: string): Metadata - return nil :: any -end - ---[=[ - @within FS - @tag must_use - - Checks if a given path is a file. - - An error will be thrown in the following situations: - - * The current process lacks permissions to read at `path`. - * Some other I/O error occurred. - - @param path The file path to check - @return If the path is a file or not -]=] -function fs.isFile(path: string): boolean - return nil :: any -end - ---[=[ - @within FS - @tag must_use - - Checks if a given path is a directory. - - An error will be thrown in the following situations: - - * The current process lacks permissions to read at `path`. - * Some other I/O error occurred. - - @param path The directory path to check - @return If the path is a directory or not -]=] -function fs.isDir(path: string): boolean - return nil :: any -end - ---[=[ - @within FS - - Moves a file or directory to a new path. - - Throws an error if a file or directory already exists at the target path. - This can be bypassed by passing `true` as the third argument, or a dictionary of options. - Refer to the documentation for `WriteOptions` for specific option keys and their values. - - An error will be thrown in the following situations: - - * The current process lacks permissions to read at `from` or write at `to`. - * The new path exists on a different mount point. - * Some other I/O error occurred. - - @param from The path to move from - @param to The path to move to - @param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists -]=] -function fs.move(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end - ---[=[ - @within FS - - Copies a file or directory recursively to a new path. - - Throws an error if a file or directory already exists at the target path. - This can be bypassed by passing `true` as the third argument, or a dictionary of options. - Refer to the documentation for `WriteOptions` for specific option keys and their values. - - An error will be thrown in the following situations: - - * The current process lacks permissions to read at `from` or write at `to`. - * Some other I/O error occurred. - - @param from The path to copy from - @param to The path to copy to - @param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists -]=] -function fs.copy(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end - -return fs diff --git a/.lune/.lune-defs/luau.luau b/.lune/.lune-defs/luau.luau deleted file mode 100644 index ab40c4f..0000000 --- a/.lune/.lune-defs/luau.luau +++ /dev/null @@ -1,123 +0,0 @@ ---[=[ - @interface CompileOptions - @within Luau - - The options passed to the luau compiler while compiling bytecode. - - This is a dictionary that may contain one or more of the following values: - - * `optimizationLevel` - Sets the compiler option "optimizationLevel". Defaults to `1`. - * `coverageLevel` - Sets the compiler option "coverageLevel". Defaults to `0`. - * `debugLevel` - Sets the compiler option "debugLevel". Defaults to `1`. - - Documentation regarding what these values represent can be found [here](https://github.com/Roblox/luau/blob/bd229816c0a82a8590395416c81c333087f541fd/Compiler/include/luacode.h#L13-L39). -]=] -export type CompileOptions = { - optimizationLevel: number?, - coverageLevel: number?, - debugLevel: number?, -} - ---[=[ - @interface LoadOptions - @within Luau - - The options passed while loading a luau chunk from an arbitrary string, or bytecode. - - This is a dictionary that may contain one or more of the following values: - - * `debugName` - The debug name of the closure. Defaults to `luau.load(...)`. - * `environment` - A custom environment to load the chunk in. Setting a custom environment will deoptimize the chunk and forcefully disable codegen. Defaults to the global environment. - * `injectGlobals` - Whether or not to inject globals in the custom environment. Has no effect if no custom environment is provided. Defaults to `true`. - * `codegenEnabled` - Whether or not to enable codegen. Defaults to `false`. -]=] -export type LoadOptions = { - debugName: string?, - environment: { [string]: any }?, - injectGlobals: boolean?, - codegenEnabled: boolean?, -} - ---[=[ - @class Luau - - Built-in library for generating luau bytecode & functions. - - ### Example usage - - ```lua - local luau = require("@lune/luau") - - local bytecode = luau.compile("print('Hello, World!')") - local callableFn = luau.load(bytecode) - - -- Additionally, we can skip the bytecode generation and load a callable function directly from the code itself. - -- local callableFn = luau.load("print('Hello, World!')") - - callableFn() - ``` - - Since luau bytecode is highly compressible, it may also make sense to compress it using the `serde` library - while transmitting large amounts of it. -]=] -local luau = {} - ---[=[ - @within Luau - - Compiles sourcecode into Luau bytecode - - An error will be thrown if the sourcecode given isn't valid Luau code. - - ### Example usage - - ```lua - local luau = require("@lune/luau") - - -- Compile the source to some highly optimized bytecode - local bytecode = luau.compile("print('Hello, World!')", { - optimizationLevel = 2, - coverageLevel = 0, - debugLevel = 1, - }) - ``` - - @param source The string that will be compiled into bytecode - @param compileOptions The options passed to the luau compiler that will output the bytecode - - @return luau bytecode -]=] -function luau.compile(source: string, compileOptions: CompileOptions?): string - return nil :: any -end - ---[=[ - @within Luau - - Generates a function from either bytecode or sourcecode - - An error will be thrown if the sourcecode given isn't valid luau code. - - ### Example usage - - ```lua - local luau = require("@lune/luau") - - local bytecode = luau.compile("print('Hello, World!')") - local callableFn = luau.load(bytecode, { - debugName = "'Hello, World'" - }) - - callableFn() - ``` - - @param source Either luau bytecode or string source code - @param loadOptions The options passed to luau for loading the chunk - - @return luau chunk -]=] -function luau.load(source: string, loadOptions: LoadOptions?): (...any) -> ...any - return nil :: any -end - -return luau diff --git a/.lune/.lune-defs/net.luau b/.lune/.lune-defs/net.luau deleted file mode 100644 index e9b793e..0000000 --- a/.lune/.lune-defs/net.luau +++ /dev/null @@ -1,321 +0,0 @@ -export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH" - -type HttpQueryOrHeaderMap = { [string]: string | { string } } -export type HttpQueryMap = HttpQueryOrHeaderMap -export type HttpHeaderMap = HttpQueryOrHeaderMap - ---[=[ - @interface FetchParamsOptions - @within Net - - Extra options for `FetchParams`. - - This is a dictionary that may contain one or more of the following values: - - * `decompress` - If the request body should be automatically decompressed when possible. Defaults to `true` -]=] -export type FetchParamsOptions = { - decompress: boolean?, -} - ---[=[ - @interface FetchParams - @within Net - - Parameters for sending network requests with `net.request`. - - This is a dictionary that may contain one or more of the following values: - - * `url` - The URL to send a request to. This is always required - * `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Defaults to `"GET"` - * `body` - The request body - * `query` - A table of key-value pairs representing query parameters in the request path - * `headers` - A table of key-value pairs representing headers - * `options` - Extra options for things such as automatic decompression of response bodies -]=] -export type FetchParams = { - url: string, - method: HttpMethod?, - body: (string | buffer)?, - query: HttpQueryMap?, - headers: HttpHeaderMap?, - options: FetchParamsOptions?, -} - ---[=[ - @interface FetchResponse - @within Net - - Response type for sending network requests with `net.request`. - - This is a dictionary containing the following values: - - * `ok` - If the status code is a canonical success status code, meaning within the range 200 -> 299 - * `statusCode` - The status code returned for the request - * `statusMessage` - The canonical status message for the returned status code, such as `"Not Found"` for status code 404 - * `headers` - A table of key-value pairs representing headers - * `body` - The request body, or an empty string if one was not given -]=] -export type FetchResponse = { - ok: boolean, - statusCode: number, - statusMessage: string, - headers: HttpHeaderMap, - body: string, -} - ---[=[ - @interface ServeRequest - @within Net - - Data type for requests in `net.serve`. - - This is a dictionary containing the following values: - - * `path` - The path being requested, relative to the root. Will be `/` if not specified - * `query` - A table of key-value pairs representing query parameters in the request path - * `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Will always be uppercase - * `headers` - A table of key-value pairs representing headers - * `body` - The request body, or an empty string if one was not given -]=] -export type ServeRequest = { - path: string, - query: { [string]: string? }, - method: HttpMethod, - headers: { [string]: string }, - body: string, -} - ---[=[ - @interface ServeResponse - @within Net - - Response type for requests in `net.serve`. - - This is a dictionary that may contain one or more of the following values: - - * `status` - The status code for the request, in the range `100` -> `599` - * `headers` - A table of key-value pairs representing headers - * `body` - The response body -]=] -export type ServeResponse = { - status: number?, - headers: { [string]: string }?, - body: (string | buffer)?, -} - -type ServeHttpHandler = (request: ServeRequest) -> string | ServeResponse -type ServeWebSocketHandler = (socket: WebSocket) -> () - ---[=[ - @interface ServeConfig - @within Net - - Configuration for `net.serve`. - - This may contain one of or more of the following values: - - * `address` for setting the IP address to serve from. Defaults to the loopback interface (`http://localhost`). - * `handleRequest` for handling normal http requests, equivalent to just passing a function to `net.serve` - * `handleWebSocket` for handling web socket requests, which will receive a `WebSocket` object as its first and only parameter - - When setting `address`, the `handleRequest` callback must also be defined. - - ```lua - net.serve(8080, { - address = "http://0.0.0.0", - handleRequest = function(request) - return { - status = 200, - body = "Echo:\n" .. request.body, - } - end - }) - ``` -]=] -export type ServeConfig = { - address: string?, - handleRequest: ServeHttpHandler?, - handleWebSocket: ServeWebSocketHandler?, -} - ---[=[ - @interface ServeHandle - @within Net - - A handle to a currently running web server, containing a single `stop` function to gracefully shut down the web server. -]=] -export type ServeHandle = { - stop: () -> (), -} - ---[=[ - @interface WebSocket - @within Net - - A reference to a web socket connection. - - The web socket may be in either an "open" or a "closed" state, changing its current behavior. - - When open: - - * Any function on the socket such as `send`, `next` or `close` can be called without erroring - * `next` can be called to yield until the next message is received or the socket becomes closed - - When closed: - - * `next` will no longer return any message(s) and instead instantly return nil - * `send` will throw an error stating that the socket has been closed - - Once the websocket has been closed, `closeCode` will no longer be nil, and will be populated with a close - code according to the [WebSocket specification](https://www.iana.org/assignments/websocket/websocket.xhtml). - This will be an integer between 1000 and 4999, where 1000 is the canonical code for normal, error-free closure. -]=] -export type WebSocket = { - closeCode: number?, - close: (code: number?) -> (), - send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (), - next: () -> string?, -} - ---[=[ - @class Net - - - Built-in library for network access - - ### Example usage - - ```lua - local net = require("@lune/net") - - -- Sending a web request - local response = net.request("https://www.google.com") - print(response.ok) - print(response.statusCode, response.statusMessage) - print(response.headers) - - -- Using a JSON web API - local response = net.request({ - url = "https://dummyjson.com/products/add", - method = "POST", - headers = { ["Content-Type"] = "application/json" }, - body = net.jsonEncode({ - title = "Cool Pencil", - }) - }) - local product = net.jsonDecode(response.body) - print(product.id, "-", product.title) - - -- Starting up a webserver - net.serve(8080, function(request) - return { - status = 200, - body = "Echo:\n" .. request.body, - } - end) - ``` -]=] -local net = {} - ---[=[ - @within Net - - Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received. - - Only throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes. - - @param config The URL or request config to use - @return A dictionary representing the response for the request -]=] -function net.request(config: string | FetchParams): FetchResponse - return nil :: any -end - ---[=[ - @within Net - @tag must_use - - Connects to a web socket at the given URL. - - Throws an error if the server at the given URL does not support - web sockets, or if a miscellaneous network or I/O error occurs. - - @param url The URL to connect to - @return A web socket handle -]=] -function net.socket(url: string): WebSocket - return nil :: any -end - ---[=[ - @within Net - - Creates an HTTP server that listens on the given `port`. - - This will ***not*** block and will keep listening for requests on the given `port` - until the `stop` function on the returned `ServeHandle` has been called. - - @param port The port to use for the server - @param handlerOrConfig The handler function or config to use for the server -]=] -function net.serve(port: number, handlerOrConfig: ServeHttpHandler | ServeConfig): ServeHandle - return nil :: any -end - ---[=[ - @within Net - @tag must_use - - Encodes the given value as JSON. - - @param value The value to encode as JSON - @param pretty If the encoded JSON string should include newlines and spaces. Defaults to false - @return The encoded JSON string -]=] -function net.jsonEncode(value: any, pretty: boolean?): string - return nil :: any -end - ---[=[ - @within Net - @tag must_use - - Decodes the given JSON string into a lua value. - - @param encoded The JSON string to decode - @return The decoded lua value -]=] -function net.jsonDecode(encoded: string): any - return nil :: any -end - ---[=[ - @within Net - @tag must_use - - Encodes the given string using URL encoding. - - @param s The string to encode - @param binary If the string should be treated as binary data and/or is not valid utf-8. Defaults to false - @return The encoded string -]=] -function net.urlEncode(s: string, binary: boolean?): string - return nil :: any -end - ---[=[ - @within Net - @tag must_use - - Decodes the given string using URL decoding. - - @param s The string to decode - @param binary If the string should be treated as binary data and/or is not valid utf-8. Defaults to false - @return The decoded string -]=] -function net.urlDecode(s: string, binary: boolean?): string - return nil :: any -end - -return net diff --git a/.lune/.lune-defs/process.luau b/.lune/.lune-defs/process.luau deleted file mode 100644 index 7b82052..0000000 --- a/.lune/.lune-defs/process.luau +++ /dev/null @@ -1,182 +0,0 @@ -export type OS = "linux" | "macos" | "windows" -export type Arch = "x86_64" | "aarch64" - -export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none" -export type SpawnOptionsStdio = { - stdout: SpawnOptionsStdioKind?, - stderr: SpawnOptionsStdioKind?, - stdin: string?, -} - ---[=[ - @interface SpawnOptions - @within Process - - A dictionary of options for `process.spawn`, with the following available values: - - * `cwd` - The current working directory for the process - * `env` - Extra environment variables to give to the process - * `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell - * `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info - * `stdin` - Optional standard input to pass to spawned child process -]=] -export type SpawnOptions = { - cwd: string?, - env: { [string]: string }?, - shell: (boolean | string)?, - stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?, - stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change -} - ---[=[ - @interface SpawnResult - @within Process - - Result type for child processes in `process.spawn`. - - This is a dictionary containing the following values: - - * `ok` - If the child process exited successfully or not, meaning the exit code was zero or not set - * `code` - The exit code set by the child process, or 0 if one was not set - * `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written - * `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written -]=] -export type SpawnResult = { - ok: boolean, - code: number, - stdout: string, - stderr: string, -} - ---[=[ - @class Process - - Built-in functions for the current process & child processes - - ### Example usage - - ```lua - local process = require("@lune/process") - - -- Getting the arguments passed to the Lune script - for index, arg in process.args do - print("Process argument #" .. tostring(index) .. ": " .. arg) - end - - -- Getting the currently available environment variables - local PORT: string? = process.env.PORT - local HOME: string? = process.env.HOME - for name, value in process.env do - print("Environment variable " .. name .. " is set to " .. value) - end - - -- Getting the current os and processor architecture - print("Running " .. process.os .. " on " .. process.arch .. "!") - - -- Spawning a child process - local result = process.spawn("program", { - "cli argument", - "other cli argument" - }) - if result.ok then - print(result.stdout) - else - print(result.stderr) - end - ``` -]=] -local process = {} - ---[=[ - @within Process - @prop os OS - @tag read_only - - The current operating system being used. - - Possible values: - - * `"linux"` - * `"macos"` - * `"windows"` -]=] -process.os = (nil :: any) :: OS - ---[=[ - @within Process - @prop arch Arch - @tag read_only - - The architecture of the processor currently being used. - - Possible values: - - * `"x86_64"` - * `"aarch64"` -]=] -process.arch = (nil :: any) :: Arch - ---[=[ - @within Process - @prop args { string } - @tag read_only - - The arguments given when running the Lune script. -]=] -process.args = (nil :: any) :: { string } - ---[=[ - @within Process - @prop cwd string - @tag read_only - - The current working directory in which the Lune script is running. -]=] -process.cwd = (nil :: any) :: string - ---[=[ - @within Process - @prop env { [string]: string? } - @tag read_write - - Current environment variables for this process. - - Setting a value on this table will set the corresponding environment variable. -]=] -process.env = (nil :: any) :: { [string]: string? } - ---[=[ - @within Process - - Exits the currently running script as soon as possible with the given exit code. - - Exit code 0 is treated as a successful exit, any other value is treated as an error. - - Setting the exit code using this function will override any otherwise automatic exit code. - - @param code The exit code to set -]=] -function process.exit(code: number?): never - return nil :: any -end - ---[=[ - @within Process - - Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process. - - The second argument, `params`, can be passed as a list of string parameters to give to the program. - - The third argument, `options`, can be passed as a dictionary of options to give to the child process. - Refer to the documentation for `SpawnOptions` for specific option keys and their values. - - @param program The program to spawn as a child process - @param params Additional parameters to pass to the program - @param options A dictionary of options for the child process - @return A dictionary representing the result of the child process -]=] -function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult - return nil :: any -end - -return process diff --git a/.lune/.lune-defs/regex.luau b/.lune/.lune-defs/regex.luau deleted file mode 100644 index 59756f3..0000000 --- a/.lune/.lune-defs/regex.luau +++ /dev/null @@ -1,218 +0,0 @@ ---[=[ - @class RegexMatch - - A match from a regular expression. - - Contains the following values: - - - `start` -- The start index of the match in the original string. - - `finish` -- The end index of the match in the original string. - - `text` -- The text that was matched. - - `len` -- The length of the text that was matched. -]=] -local RegexMatch = { - start = 0, - finish = 0, - text = "", - len = 0, -} - -type RegexMatch = typeof(RegexMatch) - ---[=[ - @class RegexCaptures - - Captures from a regular expression. -]=] -local RegexCaptures = {} - ---[=[ - @within RegexCaptures - @tag Method - - Returns the match at the given index, if one exists. - - @param index -- The index of the match to get - @return RegexMatch -- The match, if one exists -]=] -function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch? - return nil :: any -end - ---[=[ - @within RegexCaptures - @tag Method - - Returns the match for the given named match group, if one exists. - - @param group -- The name of the group to get - @return RegexMatch -- The match, if one exists -]=] -function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch? - return nil :: any -end - ---[=[ - @within RegexCaptures - @tag Method - - Formats the captures using the given format string. - - ### Example usage - - ```lua - local regex = require("@lune/regex") - - local re = regex.new("(?[0-9]{2})-(?[0-9]{2})-(?[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 diff --git a/.lune/.lune-defs/roblox.luau b/.lune/.lune-defs/roblox.luau deleted file mode 100644 index b79ad60..0000000 --- a/.lune/.lune-defs/roblox.luau +++ /dev/null @@ -1,507 +0,0 @@ -export type DatabaseScriptability = "None" | "Custom" | "Read" | "ReadWrite" | "Write" - -export type DatabasePropertyTag = - "Deprecated" - | "Hidden" - | "NotBrowsable" - | "NotReplicated" - | "NotScriptable" - | "ReadOnly" - | "WriteOnly" - -export type DatabaseClassTag = - "Deprecated" - | "NotBrowsable" - | "NotCreatable" - | "NotReplicated" - | "PlayerReplicated" - | "Service" - | "Settings" - | "UserSettings" - -export type DatabaseProperty = { - --[=[ - The name of the property. - ]=] - Name: string, - --[=[ - The datatype of the property. - - For normal datatypes this will be a string such as `string`, `Color3`, ... - - For enums this will be a string formatted as `Enum.EnumName`. - ]=] - Datatype: string, - --[=[ - The scriptability of this property, meaning if it can be written / read at runtime. - - All properties are writable and readable in Lune even if scriptability is not. - ]=] - Scriptability: DatabaseScriptability, - --[=[ - Tags describing the property. - - These include information such as if the property can be replicated to players - at runtime, if the property should be hidden in Roblox Studio, and more. - ]=] - Tags: { DatabasePropertyTag }, -} - -export type DatabaseClass = { - --[=[ - The name of the class. - ]=] - Name: string, - --[=[ - The superclass (parent class) of this class. - - May be nil if no parent class exists. - ]=] - Superclass: string?, - --[=[ - Known properties for this class. - ]=] - Properties: { [string]: DatabaseProperty }, - --[=[ - Default values for properties of this class. - - Note that these default properties use Lune's built-in datatype - userdatas, and that if there is a new datatype that Lune does - not yet know about, it may be missing from this table. - ]=] - DefaultProperties: { [string]: any }, - --[=[ - Tags describing the class. - - These include information such as if the class can be replicated - to players at runtime, and top-level class categories. - ]=] - Tags: { DatabaseClassTag }, -} - -export type DatabaseEnum = { - --[=[ - The name of this enum, for example `PartType` or `UserInputState`. - ]=] - Name: string, - --[=[ - Members of this enum. - - Note that this is a direct map of name -> enum values, - and does not actually use the EnumItem datatype itself. - ]=] - Items: { [string]: number }, -} - -export type Database = { - --[=[ - The current version of the reflection database. - - This will follow the format `x.y.z.w`, which most commonly looks something like `0.567.0.123456789` - ]=] - Version: string, - --[=[ - Retrieves a list of all currently known class names. - ]=] - GetClassNames: (self: Database) -> { string }, - --[=[ - Retrieves a list of all currently known enum names. - ]=] - GetEnumNames: (self: Database) -> { string }, - --[=[ - Gets a class with the exact given name, if one exists. - ]=] - GetClass: (self: Database, name: string) -> DatabaseClass?, - --[=[ - Gets an enum with the exact given name, if one exists. - ]=] - GetEnum: (self: Database, name: string) -> DatabaseEnum?, - --[=[ - Finds a class with the given name. - - This will use case-insensitive matching and ignore leading and trailing whitespace. - ]=] - FindClass: (self: Database, name: string) -> DatabaseClass?, - --[=[ - Finds an enum with the given name. - - This will use case-insensitive matching and ignore leading and trailing whitespace. - ]=] - FindEnum: (self: Database, name: string) -> DatabaseEnum?, -} - -type InstanceProperties = { - Parent: Instance?, - ClassName: string, - Name: string, - -- FIXME: This breaks intellisense, but we need some way to access - -- instance properties without casting the entire instance to any... - -- [string]: any, -} - -type InstanceMetatable = { - Clone: (self: Instance) -> Instance, - Destroy: (self: Instance) -> (), - ClearAllChildren: (self: Instance) -> (), - - GetChildren: (self: Instance) -> { Instance }, - GetDebugId: (self: Instance) -> string, - GetDescendants: (self: Instance) -> { Instance }, - GetFullName: (self: Instance) -> string, - - FindFirstAncestor: (self: Instance, name: string) -> Instance?, - FindFirstAncestorOfClass: (self: Instance, className: string) -> Instance?, - FindFirstAncestorWhichIsA: (self: Instance, className: string) -> Instance?, - FindFirstChild: (self: Instance, name: string, recursive: boolean?) -> Instance?, - FindFirstChildOfClass: (self: Instance, className: string, recursive: boolean?) -> Instance?, - FindFirstChildWhichIsA: (self: Instance, className: string, recursive: boolean?) -> Instance?, - - IsA: (self: Instance, className: string) -> boolean, - IsAncestorOf: (self: Instance, descendant: Instance) -> boolean, - IsDescendantOf: (self: Instance, ancestor: Instance) -> boolean, - - GetAttribute: (self: Instance, name: string) -> any, - GetAttributes: (self: Instance) -> { [string]: any }, - SetAttribute: (self: Instance, name: string, value: any) -> (), - - GetTags: (self: Instance) -> { string }, - HasTag: (self: Instance, name: string) -> boolean, - AddTag: (self: Instance, name: string) -> (), - RemoveTag: (self: Instance, name: string) -> (), -} - -export type Instance = typeof(setmetatable( - (nil :: any) :: InstanceProperties, - (nil :: any) :: { __index: InstanceMetatable } -)) - -export type DataModelProperties = {} -export type DataModelMetatable = { - GetService: (self: DataModel, name: string) -> Instance, - FindService: (self: DataModel, name: string) -> Instance?, -} - -export type DataModel = - Instance - & typeof(setmetatable( - (nil :: any) :: DataModelProperties, - (nil :: any) :: { __index: DataModelMetatable } - )) - ---[=[ - @class Roblox - - Built-in library for manipulating Roblox place & model files - - ### Example usage - - ```lua - local fs = require("@lune/fs") - local roblox = require("@lune/roblox") - - -- Reading a place file - local placeFile = fs.readFile("myPlaceFile.rbxl") - local game = roblox.deserializePlace(placeFile) - - -- Manipulating and reading instances - just like in Roblox! - local workspace = game:GetService("Workspace") - for _, child in workspace:GetChildren() do - print("Found child " .. child.Name .. " of class " .. child.ClassName) - end - - -- Writing a place file - local newPlaceFile = roblox.serializePlace(game) - fs.writeFile("myPlaceFile.rbxl", newPlaceFile) - ``` -]=] -local roblox = {} - ---[=[ - @within Roblox - @tag must_use - - Deserializes a place into a DataModel instance. - - This function accepts a string of contents, *not* a file path. - If reading a place file from a file path is desired, `fs.readFile` - can be used and the resulting string may be passed to this function. - - ### Example usage - - ```lua - local fs = require("@lune/fs") - local roblox = require("@lune/roblox") - - local placeFile = fs.readFile("filePath.rbxl") - local game = roblox.deserializePlace(placeFile) - ``` - - @param contents The contents of the place to read -]=] -function roblox.deserializePlace(contents: string): DataModel - return nil :: any -end - ---[=[ - @within Roblox - @tag must_use - - Deserializes a model into an array of instances. - - This function accepts a string of contents, *not* a file path. - If reading a model file from a file path is desired, `fs.readFile` - can be used and the resulting string may be passed to this function. - - ### Example usage - - ```lua - local fs = require("@lune/fs") - local roblox = require("@lune/roblox") - - local modelFile = fs.readFile("filePath.rbxm") - local instances = roblox.deserializeModel(modelFile) - ``` - - @param contents The contents of the model to read -]=] -function roblox.deserializeModel(contents: string): { Instance } - return nil :: any -end - ---[=[ - @within Roblox - @tag must_use - - Serializes a place from a DataModel instance. - - This string can then be written to a file, or sent over the network. - - ### Example usage - - ```lua - local fs = require("@lune/fs") - local roblox = require("@lune/roblox") - - local placeFile = roblox.serializePlace(game) - fs.writeFile("filePath.rbxl", placeFile) - ``` - - @param dataModel The DataModel for the place to serialize - @param xml If the place should be serialized as xml or not. Defaults to `false`, meaning the place gets serialized using the binary format and not xml. -]=] -function roblox.serializePlace(dataModel: DataModel, xml: boolean?): string - return nil :: any -end - ---[=[ - @within Roblox - @tag must_use - - Serializes one or more instances as a model. - - This string can then be written to a file, or sent over the network. - - ### Example usage - - ```lua - local fs = require("@lune/fs") - local roblox = require("@lune/roblox") - - local modelFile = roblox.serializeModel({ instance1, instance2, ... }) - fs.writeFile("filePath.rbxm", modelFile) - ``` - - @param instances The array of instances to serialize - @param xml If the model should be serialized as xml or not. Defaults to `false`, meaning the model gets serialized using the binary format and not xml. -]=] -function roblox.serializeModel(instances: { Instance }, xml: boolean?): string - return nil :: any -end - ---[=[ - @within Roblox - @tag must_use - - Gets the current auth cookie, for usage with Roblox web APIs. - - Note that this auth cookie is formatted for use as a "Cookie" header, - and that it contains restrictions so that it may only be used for - official Roblox endpoints. To get the raw cookie value without any - additional formatting, you can pass `true` as the first and only parameter. - - ### Example usage - - ```lua - local roblox = require("@lune/roblox") - local net = require("@lune/net") - - local cookie = roblox.getAuthCookie() - assert(cookie ~= nil, "Failed to get roblox auth cookie") - - local myPrivatePlaceId = 1234567890 - - local response = net.request({ - url = "https://assetdelivery.roblox.com/v2/assetId/" .. tostring(myPrivatePlaceId), - headers = { - Cookie = cookie, - }, - }) - - local responseTable = net.jsonDecode(response.body) - local responseLocation = responseTable.locations[1].location - print("Download link to place: " .. responseLocation) - ``` - - @param raw If the cookie should be returned as a pure value or not. Defaults to false -]=] -function roblox.getAuthCookie(raw: boolean?): string? - return nil :: any -end - ---[=[ - @within Roblox - @tag must_use - - Gets the bundled reflection database. - - This database contains information about Roblox enums, classes, and their properties. - - ### Example usage - - ```lua - local roblox = require("@lune/roblox") - - local db = roblox.getReflectionDatabase() - - print("There are", #db:GetClassNames(), "classes in the reflection database") - - print("All base instance properties:") - - local class = db:GetClass("Instance") - for name, prop in class.Properties do - print(string.format( - "- %s with datatype %s and default value %s", - prop.Name, - prop.Datatype, - tostring(class.DefaultProperties[prop.Name]) - )) - end - ``` -]=] -function roblox.getReflectionDatabase(): Database - return nil :: any -end - ---[=[ - @within Roblox - - Implements a property for all instances of the given `className`. - - This takes into account class hierarchies, so implementing a property - for the `BasePart` class will also implement it for `Part` and others, - unless a more specific implementation is added to the `Part` class directly. - - ### Behavior - - The given `getter` callback will be called each time the property is - indexed, with the instance as its one and only argument. The `setter` - callback, if given, will be called each time the property should be set, - with the instance as the first argument and the property value as second. - - ### Example usage - - ```lua - local roblox = require("@lune/roblox") - - local part = roblox.Instance.new("Part") - - local propertyValues = {} - roblox.implementProperty( - "BasePart", - "CoolProp", - function(instance) - if propertyValues[instance] == nil then - propertyValues[instance] = 0 - end - propertyValues[instance] += 1 - return propertyValues[instance] - end, - function(instance, value) - propertyValues[instance] = value - end - ) - - print(part.CoolProp) --> 1 - print(part.CoolProp) --> 2 - print(part.CoolProp) --> 3 - - part.CoolProp = 10 - - print(part.CoolProp) --> 11 - print(part.CoolProp) --> 12 - print(part.CoolProp) --> 13 - ``` - - @param className The class to implement the property for. - @param propertyName The name of the property to implement. - @param getter The function which will be called to get the property value when indexed. - @param setter The function which will be called to set the property value when indexed. Defaults to a function that will error with a message saying the property is read-only. -]=] -function roblox.implementProperty( - 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 diff --git a/.lune/.lune-defs/serde.luau b/.lune/.lune-defs/serde.luau deleted file mode 100644 index cd2658d..0000000 --- a/.lune/.lune-defs/serde.luau +++ /dev/null @@ -1,200 +0,0 @@ ---[=[ - @within Serde - @interface EncodeDecodeFormat - - A serialization/deserialization format supported by the Serde library. - - Currently supported formats: - - | Name | Learn More | - |:-------|:---------------------| - | `json` | https://www.json.org | - | `yaml` | https://yaml.org | - | `toml` | https://toml.io | -]=] -export type EncodeDecodeFormat = "json" | "yaml" | "toml" - ---[=[ - @within Serde - @interface CompressDecompressFormat - - A compression/decompression format supported by the Serde library. - - Currently supported formats: - - | Name | Learn More | - |:---------|:----------------------------------| - | `brotli` | https://github.com/google/brotli | - | `gzip` | https://www.gnu.org/software/gzip | - | `lz4` | https://github.com/lz4/lz4 | - | `zlib` | https://www.zlib.net | -]=] -export type CompressDecompressFormat = "brotli" | "gzip" | "lz4" | "zlib" - ---[=[ - @within Serde - @interface HashAlgorithm - - A hash algorithm supported by the Serde library. - - Currently supported algorithms: - - | Name | Learn More | - |:-----------|:-------------------------------------| - | `md5` | https://en.wikipedia.org/wiki/MD5 | - | `sha1` | https://en.wikipedia.org/wiki/SHA-1 | - | `sha224` | https://en.wikipedia.org/wiki/SHA-2 | - | `sha256` | https://en.wikipedia.org/wiki/SHA-2 | - | `sha384` | https://en.wikipedia.org/wiki/SHA-2 | - | `sha512` | https://en.wikipedia.org/wiki/SHA-2 | - | `sha3-224` | https://en.wikipedia.org/wiki/SHA-3 | - | `sha3-256` | https://en.wikipedia.org/wiki/SHA-3 | - | `sha3-384` | https://en.wikipedia.org/wiki/SHA-3 | - | `sha3-512` | https://en.wikipedia.org/wiki/SHA-3 | - | `blake3` | https://en.wikipedia.org/wiki/BLAKE3 | -]=] -export type HashAlgorithm = - "md5" - | "sha1" - | "sha224" - | "sha256" - | "sha384" - | "sha512" - | "sha3-224" - | "sha3-256" - | "sha3-384" - | "sha3-512" - | "blake3" - ---[=[ - @class Serde - - Built-in library for: - - serialization & deserialization - - encoding & decoding - - compression - - ### Example usage - - ```lua - local fs = require("@lune/fs") - local serde = require("@lune/serde") - - -- Parse different file formats into lua tables - local someJson = serde.decode("json", fs.readFile("myFile.json")) - local someToml = serde.decode("toml", fs.readFile("myFile.toml")) - local someYaml = serde.decode("yaml", fs.readFile("myFile.yaml")) - - -- Write lua tables to files in different formats - fs.writeFile("myFile.json", serde.encode("json", someJson)) - fs.writeFile("myFile.toml", serde.encode("toml", someToml)) - fs.writeFile("myFile.yaml", serde.encode("yaml", someYaml)) - ``` -]=] -local serde = {} - ---[=[ - @within Serde - @tag must_use - - Encodes the given value using the given format. - - See [`EncodeDecodeFormat`] for a list of supported formats. - - @param format The format to use - @param value The value to encode - @param pretty If the encoded string should be human-readable, including things such as newlines and spaces. Only supported for json and toml formats, and defaults to false - @return The encoded string -]=] -function serde.encode(format: EncodeDecodeFormat, value: any, pretty: boolean?): string - return nil :: any -end - ---[=[ - @within Serde - @tag must_use - - Decodes the given string using the given format into a lua value. - - See [`EncodeDecodeFormat`] for a list of supported formats. - - @param format The format to use - @param encoded The string to decode - @return The decoded lua value -]=] -function serde.decode(format: EncodeDecodeFormat, encoded: buffer | string): any - return nil :: any -end - ---[=[ - @within Serde - @tag must_use - - Compresses the given string using the given format. - - See [`CompressDecompressFormat`] for a list of supported formats. - - @param format The format to use - @param s The string to compress - @param level The compression level to use, clamped to the format's limits. The best compression level is used by default - @return The compressed string -]=] -function serde.compress(format: CompressDecompressFormat, s: buffer | string, level: number?): string - return nil :: any -end - ---[=[ - @within Serde - @tag must_use - - Decompresses the given string using the given format. - - See [`CompressDecompressFormat`] for a list of supported formats. - - @param format The format to use - @param s The string to decompress - @return The decompressed string -]=] -function serde.decompress(format: CompressDecompressFormat, s: buffer | string): string - return nil :: any -end - ---[=[ - @within Serde - @tag must_use - - Hashes the given message using the given algorithm - and returns the hash as a hex string. - - See [`HashAlgorithm`] for a list of supported algorithms. - - @param algorithm The algorithm to use - @param message The message to hash - @return The hash as a hex string -]=] -function serde.hash(algorithm: HashAlgorithm, message: string | buffer): string - return nil :: any -end - ---[=[ - @within Serde - @tag must_use - - Hashes the given message using HMAC with the given secret - and algorithm, returning the hash as a base64 string. - - See [`HashAlgorithm`] for a list of supported algorithms. - - @param algorithm The algorithm to use - @param message The message to hash - @return The hash as a base64 string -]=] -function serde.hmac( - algorithm: HashAlgorithm, - message: string | buffer, - secret: string | buffer -): string - return nil :: any -end - -return serde diff --git a/.lune/.lune-defs/stdio.luau b/.lune/.lune-defs/stdio.luau deleted file mode 100644 index e6e88a4..0000000 --- a/.lune/.lune-defs/stdio.luau +++ /dev/null @@ -1,161 +0,0 @@ -export type Color = - "reset" - | "black" - | "red" - | "green" - | "yellow" - | "blue" - | "purple" - | "cyan" - | "white" -export type Style = "reset" | "bold" | "dim" - -type PromptFn = ( - (() -> string) - & ((kind: "text", message: string?, defaultOrOptions: string?) -> string) - & ((kind: "confirm", message: string, defaultOrOptions: boolean?) -> boolean) - & ((kind: "select", message: string?, defaultOrOptions: { string }) -> number?) - & ((kind: "multiselect", message: string?, defaultOrOptions: { string }) -> { number }?) -) - ---[=[ - @within Stdio - @function prompt - @tag must_use - - Prompts for user input using the wanted kind of prompt: - - * `"text"` - Prompts for a plain text string from the user - * `"confirm"` - Prompts the user to confirm with y / n (yes / no) - * `"select"` - Prompts the user to select *one* value from a list - * `"multiselect"` - Prompts the user to select *one or more* values from a list - * `nil` - Equivalent to `"text"` with no extra arguments - - @param kind The kind of prompt to use - @param message The message to show the user - @param defaultOrOptions The default value for the prompt, or options to choose from for selection prompts -]=] -local prompt: PromptFn = function(kind: any, message: any, defaultOrOptions: any) - return nil :: any -end - ---[=[ - @class Stdio - - Built-in standard input / output & utility functions - - ### Example usage - - ```lua - local stdio = require("@lune/stdio") - - -- Prompting the user for basic input - local text: string = stdio.prompt("text", "Please write some text") - local confirmed: boolean = stdio.prompt("confirm", "Please confirm this action") - - -- Writing directly to stdout or stderr, without the auto-formatting of print/warn/error - stdio.write("Hello, ") - stdio.write("World! ") - stdio.write("All on the same line") - stdio.ewrite("\nAnd some error text, too") - - -- Reading the entire input from stdin - local input = stdio.readToEnd() - ``` -]=] -local stdio = {} - -stdio.prompt = prompt - ---[=[ - @within Stdio - @tag must_use - - Return an ANSI string that can be used to modify the persistent output color. - - Pass `"reset"` to get a string that can reset the persistent output color. - - ### Example usage - - ```lua - stdio.write(stdio.color("red")) - print("This text will be red") - stdio.write(stdio.color("reset")) - print("This text will be normal") - ``` - - @param color The color to use - @return A printable ANSI string -]=] -function stdio.color(color: Color): string - return nil :: any -end - ---[=[ - @within Stdio - @tag must_use - - Return an ANSI string that can be used to modify the persistent output style. - - Pass `"reset"` to get a string that can reset the persistent output style. - - ### Example usage - - ```lua - stdio.write(stdio.style("bold")) - print("This text will be bold") - stdio.write(stdio.style("reset")) - print("This text will be normal") - ``` - - @param style The style to use - @return A printable ANSI string -]=] -function stdio.style(style: Style): string - return nil :: any -end - ---[=[ - @within Stdio - @tag must_use - - Formats arguments into a human-readable string with syntax highlighting for tables. - - @param ... The values to format - @return The formatted string -]=] -function stdio.format(...: any): string - return nil :: any -end - ---[=[ - @within Stdio - - Writes a string directly to stdout, without any newline. - - @param s The string to write to stdout -]=] -function stdio.write(s: string) end - ---[=[ - @within Stdio - - Writes a string directly to stderr, without any newline. - - @param s The string to write to stderr -]=] -function stdio.ewrite(s: string) end - ---[=[ - @within Stdio - @tag must_use - - Reads the entire input from stdin. - - @return The input from stdin -]=] -function stdio.readToEnd(): string - return nil :: any -end - -return stdio diff --git a/.lune/.lune-defs/task.luau b/.lune/.lune-defs/task.luau deleted file mode 100644 index 81bdc2f..0000000 --- a/.lune/.lune-defs/task.luau +++ /dev/null @@ -1,99 +0,0 @@ ---[=[ - @class Task - - Built-in task scheduler & thread spawning - - ### Example usage - - ```lua - local task = require("@lune/task") - - -- Waiting for a certain amount of time - task.wait(1) - print("Waited for one second") - - -- Running a task after a given amount of time - task.delay(2, function() - print("Ran after two seconds") - end) - - -- Spawning a new task that runs concurrently - task.spawn(function() - print("Running instantly") - task.wait(1) - print("One second passed inside the task") - end) - - print("Running after task.spawn yields") - ``` -]=] -local task = {} - ---[=[ - @within Task - - Stops a currently scheduled thread from resuming. - - @param thread The thread to cancel -]=] -function task.cancel(thread: thread) end - ---[=[ - @within Task - - Defers a thread or function to run at the end of the current task queue. - - @param functionOrThread The function or thread to defer - @return The thread that will be deferred -]=] -function task.defer(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( - 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(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 diff --git a/.lune/analyze.luau b/.lune/analyze.luau deleted file mode 100644 index 6f884d6..0000000 --- a/.lune/analyze.luau +++ /dev/null @@ -1,8 +0,0 @@ ---!strict -local spawn = require("util/spawn") - -spawn.start("lune run install-packages") -spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") -spawn.start( - "luau-lsp analyze --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore Packages/**/*.lua --ignore Packages/**/*.luau --ignore Packages/*.lua --ignore Packages/*.luau lib/" -) diff --git a/.lune/build.luau b/.lune/build.luau index 8be60df..a149ed4 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -1,7 +1,4 @@ --!strict -local spawn = require("util/spawn") +local spawn = require("./util/spawn") -spawn.start("lune run install-packages") -spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") -spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } }) -spawn.start("rojo build default.project.json -o build.rbxm") +spawn.start("rojo build default.project.json -o hammer.rbxm") diff --git a/.lune/check.luau b/.lune/check.luau index 61712a2..042f118 100644 --- a/.lune/check.luau +++ b/.lune/check.luau @@ -1,8 +1,6 @@ --!strict -local spawn = require("util/spawn") +local spawn = require("./util/spawn") -spawn.start("lune run analyze") spawn.start("stylua lib/") spawn.start("selene lib/") -spawn.start("lune run download-jecs") spawn.start("luau test/tests.luau") diff --git a/.lune/dev.luau b/.lune/dev.luau index 188dffd..4b71341 100644 --- a/.lune/dev.luau +++ b/.lune/dev.luau @@ -1,57 +1,10 @@ --!strict -local process = require("@lune/process") -local stdio = require("@lune/stdio") local task = require("@lune/task") -local spawn = require("util/spawn") -local watch = require("util/watch") +local spawn = require("./util/spawn") +local watch = require("./util/watch") -task.spawn(watch, "wally.toml", function() - spawn.spawn("lune run install-packages") +task.spawn(watch, "pesde.toml", function() + spawn.spawn("pesde install") end, false) -spawn.start("lune run install-packages") - -spawn.spawn("rojo sourcemap dev.project.json -o sourcemap.json --watch") -spawn.spawn("darklua process --config .darklua.json --watch lib/ dist/", { env = { ROBLOX_DEV = "true" } }) - -task.wait(2.5) - -while true do - local start_commit = stdio.prompt("confirm", "Start commit? -- `y` to start a commit, `n` to exit the script") - if not start_commit then - process.exit(0) - break - end - - local _, check_result = pcall(spawn.start, "lune run check") - if not check_result.ok then - warn("Check didn't go ok, aborting commit") - break - end - - local commit_title = stdio.prompt("text", "Commit title -- leave blank to stop committing") - if not commit_title or commit_title == "" then - print("Stopping commit") - continue - end - local commit_messages = { `-m`, commit_title } - - while true do - local commit_message = stdio.prompt("text", "Commit message -- added to the description, leave blank to finish") - if not commit_message or commit_message == "" then - break - end - - table.insert(commit_messages, "-m") - table.insert(commit_messages, commit_message) - end - - local confirm = stdio.prompt("confirm", "Confirm?") - if not confirm then - break - end - - spawn.start("git add .") - process.spawn("git", { "commit", unpack(commit_messages) }, { stdio = "forward" }) - spawn.start("git push") -end +spawn.start("pesde install") diff --git a/.lune/dist.luau b/.lune/dist.luau deleted file mode 100644 index b6ba7f2..0000000 --- a/.lune/dist.luau +++ /dev/null @@ -1,21 +0,0 @@ ---!strict -local fs = require("@lune/fs") -local spawn = require("util/spawn") - -spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") -spawn.start("lune run install-packages") -spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } }) - -for _, path in fs.readDir("dist") do - path = `dist/{path}` - if not fs.isFile(path) then - continue - end - - print("found working file") - - local file = fs.readFile(path) - local new_contents = - string.gsub(file, `require%("%.%./jecs"%)`, `require(script.Parent.Parent:FindFirstChild('jecs'))`) - fs.writeFile(path, new_contents) -end diff --git a/.lune/download-jecs.luau b/.lune/download-jecs.luau deleted file mode 100644 index e3fb185..0000000 --- a/.lune/download-jecs.luau +++ /dev/null @@ -1,69 +0,0 @@ ---!strict -local fs = require("@lune/fs") -local net = require("@lune/net") -local process = require("@lune/process") -local serde = require("@lune/serde") -local spawn = require("util/spawn") - -type wally_manifest = { - package: { - name: string, - version: string, - registry: string, - realm: string, - license: string?, - exclude: { string }?, - include: { string }?, - }, - dependencies: { - [string]: string, - }, -} - -local github_token: string = process.args[1] - -if not github_token then - local env_exists = fs.metadata(".env").exists - if not env_exists then - error("Usage: lune run download-jecs [GITHUB_PAT]\nAlternatively, put the PAT in an .env file under GITHUB_PAT") - end - - local env = serde.decode("toml", fs.readFile(".env")) - local pat = env.GITHUB_PAT or error("Couldn't read GITHUB_PAT from .env") - github_token = pat -end - -local manifest_contents = fs.readFile("wally.toml") or error("Couldn't read manifest.") -local manifest: wally_manifest = serde.decode("toml", manifest_contents) or error("Couldn't decode manifest.") -local jecs_version = string.match(manifest.dependencies.jecs, "%d.%d.%d") or error("Couldn't find jecs version.") - -type gh_api_tag = { - ref: string, - node_id: string, - url: string, - object: { - sha: string, - type: string, - url: string, - }, -} - -local response = net.request({ - url = `https://api.github.com/repos/ukendio/jecs/git/refs/tags/v{jecs_version}`, - method = "GET", - headers = { - Accept = "application/vnd.github+json", - Authorization = `Bearer {github_token}`, - ["X-GitHub-Api-Version"] = "2022-11-28", - }, -}) - -if not response.ok then - error(`Github api response not ok:\n{response.statusCode} @ {response.statusMessage}\n{response.body}`) -end - -local gh_api_tag: gh_api_tag = serde.decode("json", response.body) - -spawn.start( - `curl https://raw.githubusercontent.com/ukendio/jecs/{gh_api_tag.object.sha}/src/init.luau -o jecs_src.luau` -) diff --git a/.lune/install-packages.luau b/.lune/install-packages.luau deleted file mode 100644 index c6b363b..0000000 --- a/.lune/install-packages.luau +++ /dev/null @@ -1,6 +0,0 @@ ---!strict -local spawn = require("util/spawn") - -spawn.start("wally install") -spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") -spawn.start("wally-package-types --sourcemap sourcemap.json Packages/") diff --git a/.lune/util/spawn.luau b/.lune/util/spawn.luau index 6e46e5d..0ec4e7f 100644 --- a/.lune/util/spawn.luau +++ b/.lune/util/spawn.luau @@ -9,15 +9,15 @@ local task = require("@lune/task") --- @param cmd string --- @param options process.SpawnOptions? --- @return process.SpawnResult -local function start_process(cmd: string, options: process.SpawnOptions?): process.SpawnResult +local function start_process(cmd: string, options: process.ExecOptions?): process.ExecResult local arguments = string.split(cmd, " ") local command = arguments[1] table.remove(arguments, 1) - local opts: process.SpawnOptions = options ~= nil and options or {} - opts.stdio = opts.stdio ~= nil and opts.stdio or "forward" + local opts: process.ExecOptions = options ~= nil and options or {} + opts.stdio = opts.stdio ~= nil and opts.stdio or "forward" :: any - return process.spawn(command, arguments, opts) + return process.exec(command, arguments, opts) end --- `task.spawn` a process with the given command and options @@ -27,7 +27,7 @@ end --- @param cmd string --- @param options process.SpawnOptions? --- @return process.SpawnResult -local function spawn_process(cmd: string, options: process.SpawnOptions?) +local function spawn_process(cmd: string, options: process.ExecOptions?) task.spawn(start_process, cmd, options) end diff --git a/.zed/settings.json b/.zed/settings.json index aff71f6..0a07aaf 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,44 +1,47 @@ // Folder-specific settings // // For a full list of overridable settings, and general information on folder-specific settings, -// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files { "lsp": { "luau-lsp": { "settings": { "luau-lsp": { + "diagnostics": { + "workspace": false + }, "completion": { "imports": { "enabled": true, "suggestServices": true, - "suggestRequires": false + "suggestRequires": true, + "stringRequires": { + "enabled": true + } } }, - "sourcemap": { - "rojoProjectFile": "dev.project.json" - }, "require": { "mode": "relativeToFile" } }, "ext": { "roblox": { - "enabled": false + "enabled": false, + "security_level": "roblox_script" }, "fflags": { - "override": { - "LuauTinyControlFlowAnalysis": "true" - }, + "enable_new_solver": true, "sync": true, "enable_by_default": false + }, + "binary": { + "ignore_system_version": false } } } } }, - "languages": { - "TypeScript": { - "tab_size": 4 - } + "file_types": { + "Luau": ["lua"] } } diff --git a/LICENSE b/LICENSE index 65ed78b..1f7ab96 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Mark-Marks +Copyright (c) 2025 marked Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index adb85a1..488164b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,113 @@ -# sapphire-utils -[![CI](https://img.shields.io/github/actions/workflow/status/mark-marks/jecs-utils/ci.yml?style=for-the-badge&label=CI)](https://github.com/mark-marks/jecs-utils/actions/workflows/ci.yml) -[![Wally](https://img.shields.io/github/v/tag/mark-marks/jecs-utils?&style=for-the-badge)](https://wally.run/package/mark-marks/jecs-utils) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://github.com/Mark-Marks/jecs-utils/blob/main/LICENSE) +

+ +

+ +[![CI](https://git.devmarked.win/marked/hammer/badges/workflows/ci.yml/badge.svg?style=for-the-badge&label=CI)](https://git.devmarked.win/marked/hammer/actions?workflow=ci.yml) +[![CD](https://git.devmarked.win/marked/hammer/badges/workflows/release.yml/badge.svg?style=for-the-badge&label=RELEASE)](https://git.devmarked.win/marked/hammer/actions?workflow=release.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://git.devmarked.win/marked/hammer/src/branch/main/LICENSE) +Wally +Pesde A set of utilities for [Jecs](https://github.com/ukendio/jecs)
-## Features +## Installation -- [collect](/lib/collect.luau) - Collects all arguments fired through the given signal, and drains the collection on iteration. -- [handle](/lib/handle.luau) - Wrap `jecs.World` functions for faster (DX wise) operating on entities -- [replicator](/lib/replicator.luau) - Keep track of all entities with the passed components and calculate differences -- [ref](/lib/ref.luau) - Reference entities by key -- [command_buffer](/lib/command_buffer.luau) - Buffer commands to prevent iterator invalidation -- [spawner](/lib/spawner.luau) - Spawn entities with required components +Hammer is available on pesde @ `marked/hammer` and Wally @ `mark-marks/hammer`. + +## Usage + +All utilities that require a Jecs world to function are exposed via a constructor pattern.\ +For instance, to build a `ref`: +```luau +local ref = hammer.ref(world) +``` +This is the easiest solution for passing a world that doesn't sacrifice readability internally and externally or bind the developer to a Jecs version that hammer is currently using. + +### collect + +A [collect](/lib/utilities/collect.luau) collects all arguments fired through the given signal, and exposes an iterator for them.\ +Its purpose is to interface with signals in ECS code, which ideally should run every frame in a loop. + +For instance, take Roblox's RemoteEvents: +```luau +local pings = hammer.collect(events.ping) +local function system() + for _, player, ping in pings do + events.ping:FireClient(player, "pong!") + end +end +``` + +### command_buffer + +A [command_buffer](/lib/utilities/command_buffer.luau) lets you buffer world commands in order to prevent iterator invalidation.\ +Iterator invalidation refers to an iterator (e.g. `world:query(Component)`) becoming unusable due to changes in the underlying data. + +To prevent this, command buffers can be used to delay world operations to the end of the current frame: +```luau +local command_buffer = hammer.command_buffer(world) + +while true do + step_systems() + command_buffer.flush() +end + +-- Inside a system: +command_buffer.add(entity, component) -- This runs after all of the systems run; no data changes while things are running +``` + +### ref + +A [ref](/lib/utilities/ref.luau) allows for storing and getting entities via some form of reference.\ +This is particularly useful for situations where you reconcile entities into your world from a foreign place, e.g. from across a networking boundary. +```luau +local ref = hammer.ref(world) + +for id in net.new_entities.iter() do + local entity = ref(`foreign-{id}`) -- A new entity that can be tracked via a foreign id +end +``` + +Refs by default create a new entity if the given value doesn't reference any stored one. In case you want to see if a reference exists, you can find one: +```luau +local entity[: Entity?] = ref.find(`my-key`) +``` + +Refs can also be deleted. All functions used to a fetch a reference also return a cleanup function: +```luau +local entity, destroy_reference = ref(`my-key`) +destroy_reference() -- `entity` still persists in the world, but `my-key` doesn't refer to it anymore. +``` + +Refs are automatically cached by world. `ref(world)` will have the same underlying references as `ref(world)`.\ +In case you need an unique reference store, you can omit the cache via `ref(world, true)`. + +### tracker + +A [tracker](/lib/utilities/tracker) keeps a history of all components passed to it, and how to get to their current state in the least amount of commands.\ +They're great for replicating world state across a networking barrier, as you're able to easily get diffed snapshots and apply them. +```luau +local tracker = hammer.tracker(world, ComponentA, ComponentB) + +world:set(entity_a, ComponentA, 50) +world:add(entity_b, ComponentB) + +-- Says how to give `entity_a` `ComponentA` with the value of `50` and give `entity_b` `ComponentB`. +-- `state()` always tracks from when the tracker was first created. +local state = tracker.state() + +-- Same as the above, but now this sets the origin for the next taken snapshot! +local snapshot = tracker.snapshot() + +world:remove(entity_b, ComponentB) + +-- This now only says to remove `ComponentB` from `entity_b`. +local snapshot_b = tracker.snapshot() +``` + +Trackers simplify the state internally. Removals remove all prior commands pertaining to the entity and component pair, adds remove all prior removals, etc. + +Trackers are optimized under the hood with lookup tables for arrays, to allow for a constant time operation to check for whether it has a member or not. It can lead to worse memory usage, but makes it faster overall. diff --git a/assets/hammer-logo.png b/assets/hammer-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bc9f48a2dbebf510637493088f2cc09cb55a866a GIT binary patch literal 37746 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 literal 0 HcmV?d00001 diff --git a/default.project.json b/default.project.json index 047a782..959b2a6 100644 --- a/default.project.json +++ b/default.project.json @@ -1,6 +1,6 @@ { - "name": "jecs-utils", + "name": "hammer", "tree": { - "$path": "dist" + "$path": "lib" } } diff --git a/dev.project.json b/dev.project.json deleted file mode 100644 index 5783bdf..0000000 --- a/dev.project.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "dev", - "tree": { - "$className": "DataModel", - - "ReplicatedStorage": { - "Packages": { - "$className": "Folder", - "$path": "Packages", - "jecs": { - "$path": "jecs.luau" - }, - "jecs_utils": { - "$path": "lib" - } - } - } - } -} diff --git a/jecs.luau b/jecs.luau index 8fc5816..86e5a2d 100644 --- a/jecs.luau +++ b/jecs.luau @@ -6,8 +6,8 @@ -- this is here to mitigate that local jecs = require("./luau_packages/jecs") export type Archetype = jecs.Archetype -export type Id = jecs.Id +export type Id = jecs.Id export type Pair = jecs.Pair -export type Entity = jecs.Entity +export type Entity = jecs.Entity export type World = jecs.World return jecs diff --git a/lib/command_buffer.luau b/lib/command_buffer.luau deleted file mode 100644 index 08ccd52..0000000 --- a/lib/command_buffer.luau +++ /dev/null @@ -1,135 +0,0 @@ ---!strict ---!optimize 2 -local jecs = require("../jecs") -type entity = jecs.Entity -type id = jecs.Id - -local _world = require("./world") -local WORLD = _world.get - --- luau-lsp literally dies if you use the actual world type -type jecs_world = any - ---- `map>` -local add_commands: { [jecs_world]: { [id]: { entity } } } = {} ---- `map>` -local set_commands: { [jecs_world]: { [id]: { [entity]: any } } } = {} ---- `map>` -local remove_commands: { [jecs_world]: { [id]: { entity } } } = {} ---- `array` -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: (entity: entity, component: id, 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(entity: entity, component: id, 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 diff --git a/lib/handle.luau b/lib/handle.luau deleted file mode 100644 index 70bc7e2..0000000 --- a/lib/handle.luau +++ /dev/null @@ -1,78 +0,0 @@ ---!strict ---!optimize 2 -local jecs = require("../jecs") -type entity = jecs.Entity -type id = jecs.Id - -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: ((self: handle, id) -> A?) - & ((self: handle, id, id) -> (A?, B?)) - & ((self: handle, id, id, id) -> (A?, B?, C?)) - & ((self: handle, id, id, id, id) -> (A?, B?, C?, D?)), - --- Adds a component to the entity with no value - add: (self: handle, id: id) -> handle, - --- Assigns a value to a component on the given entity - set: (self: handle, id: id, 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(id: id): handle - self.world:add(self.entity, id) - return self -end - -function handle:set(id: id, 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 diff --git a/lib/init.luau b/lib/init.luau index 8bcbb66..9c4f483 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -1,42 +1,22 @@ --!strict --!optimize 2 -local jecs = require("../jecs") +local collect = require("@self/utilities/collect") +export type SignalLike = collect.SignalLike +export type VerboseSignalLike = collect.SignalLike -local WORLD = require("./world") +local ref = require("@self/utilities/ref") +export type Ref = ref.Identity -local collect = require("./collect") -export type collect_signal_like = collect.signal_like -export type collect_verbose_signal_like = collect.signal_like +local tracker = require("@self/utilities/tracker") +export type Tracker = tracker.Identity +export type TrackerCommands = tracker.Commands -local command_buffer = require("./command_buffer") -export type command_buffer = command_buffer.command_buffer - -local handle = require("./handle") -export type handle = handle.handle - -local ref = require("./ref") - -local replicator = require("./replicator") -export type replicator = replicator.replicator -export type changes = replicator.changes - -local spawner = require("./spawner") -export type spawner = spawner.spawner - ---- Set the world for all utilities. ---- Should be called once per context before any utility is used. ---- @param world jecs.World -local function initialize(world: jecs.World) - WORLD.set(world) -end +local command_buffer = require("@self/utilities/command_buffer") +export type CommandBuffer = command_buffer.Identity return { - initialize = initialize, - collect = collect, - handle = handle, - replicator = replicator, ref = ref, + tracker = tracker, command_buffer = command_buffer, - spawner = spawner, } diff --git a/lib/ref.luau b/lib/ref.luau deleted file mode 100644 index 7975dca..0000000 --- a/lib/ref.luau +++ /dev/null @@ -1,67 +0,0 @@ ---!strict ---!optimize 2 -local handle = require("./handle") -local jecs = require("../jecs") -local WORLD = require("./world").get - -local refs: { [jecs.World]: { [any]: jecs.Entity } } = {} - -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 diff --git a/lib/replicator.luau b/lib/replicator.luau deleted file mode 100644 index d65e88f..0000000 --- a/lib/replicator.luau +++ /dev/null @@ -1,247 +0,0 @@ ---!strict ---!optimize 2 -local jecs = require("../jecs") -type entity = jecs.Entity -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>` -type changes_added = { [i53]: { i53 } } ---- `map>` -type changes_set = { [i53]: { [i53]: unknown } } ---- `map>` -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 diff --git a/lib/spawner.luau b/lib/spawner.luau deleted file mode 100644 index ccad166..0000000 --- a/lib/spawner.luau +++ /dev/null @@ -1,49 +0,0 @@ ---!strict -local spawner_type = require("./spawner_type") -local WORLD = require("./world").get -local handle = require("./handle") - -export type spawner = spawner_type.spawner - ---- 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 -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 diff --git a/lib/spawner_type.luau b/lib/spawner_type.luau deleted file mode 100644 index 3c9e5a3..0000000 --- a/lib/spawner_type.luau +++ /dev/null @@ -1,391 +0,0 @@ ---!strict -local jecs = require("../jecs") -type entity = jecs.Entity -type id = jecs.Id - -local handle = require("./handle") - -export type spawner = { - --- 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` vs `T`) -export type create_spawner = - ((id) -> spawner) - & ((id, id) -> spawner) - & ((id, id, id) -> spawner) - & ((id, id, id, id) -> spawner) - & ((id, id, id, id, id) -> spawner) - & ((id, id, id, id, id, id) -> spawner) - & ((id, id, id, id, id, id, id) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id

, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id - ) -> spawner) - -return {} diff --git a/lib/collect.luau b/lib/utilities/collect.luau similarity index 74% rename from lib/collect.luau rename to lib/utilities/collect.luau index e5629ef..7531532 100644 --- a/lib/collect.luau +++ b/lib/utilities/collect.luau @@ -1,6 +1,5 @@ --!strict --!optimize 2 - --[[ original author by @memorycode @@ -28,24 +27,12 @@ SOFTWARE. --]] --- What signals passed to `collect()` should be able to be coerced into -export type signal_like = { connect: confn, [any]: any } | { Connect: confn, [any]: any } -type confn = (self: signal_like, (T...) -> ()) -> D +export type SignalLike = { connect: Connector, [any]: any } | { Connect: Connector, [any]: any } +type Connector = (self: SignalLike, (T...) -> ()) -> D --- Collects all arguments fired through the given signal, and drains the collection on iteration.\ ---- Expects signals to have a `Connect` ***method***. ---- ```luau ---- local sig = collect(some_signal) ---- ---- -- Imagine this as an ECS scheduler loop ---- while task.wait() do ---- for index, arg1 in sig do -- arg1, arg2, etc ---- print(arg1) ---- end ---- end ---- ``` ---- @param event signal ---- @return () -> (number, T...), D -- iterator and disconnector -local function collect(event: signal_like): (() -> (number, T...), D) +--- Expects signals to have a `Connect` or `connect` ***method***. +local function collect(event: SignalLike): (() -> (number, T...), D) local storage = {} local mt = {} local iter = function() @@ -53,11 +40,11 @@ local function collect(event: signal_like): (() -> (number, T. return function(): (number?, T...) if n <= 0 then mt.__iter = nil - return nil + return nil :: any end n -= 1 - return n + 1, unpack(table.remove(storage, 1) :: any) + return n + 1, unpack(table.remove(storage, 1) :: any) :: any end end diff --git a/lib/utilities/command_buffer.luau b/lib/utilities/command_buffer.luau new file mode 100644 index 0000000..a9130bb --- /dev/null +++ b/lib/utilities/command_buffer.luau @@ -0,0 +1,140 @@ +--!strict +--!optimize 2 +local jecs = require("../../jecs") +type Entity = jecs.Entity +type Id = jecs.Id +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: (entity: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (entity: Entity, component: Id, data: T) -> (), + --- Removes a component from the given entity + remove: (entity: Entity, component: Id) -> (), + --- 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(entity: Entity, component: Id) + local cmds = add_commands[component] + if not cmds then + cmds = {} + add_commands[component] = cmds + end + + table.insert(cmds, entity) + end + + local function set(entity: Entity, component: Id, data: T) + local cmds = set_commands[component] + if not cmds then + cmds = {} + set_commands[component] = cmds + end + + cmds[entity] = data + end + + local function remove(entity: Entity, component: Id) + 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 diff --git a/lib/utilities/ref.luau b/lib/utilities/ref.luau new file mode 100644 index 0000000..f74095a --- /dev/null +++ b/lib/utilities/ref.luau @@ -0,0 +1,85 @@ +--!strict +--!optimize 2 +local jecs = require("../../jecs") +type Entity = jecs.Entity + +export type Identity = typeof(setmetatable( + {}, + {} :: { + __call: (any, key: unknown) -> (Entity, Cleaner), + __index: { + reference: (key: unknown) -> (Entity, Cleaner), + find: (key: unknown) -> (Entity?, 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(key: unknown): (Entity, 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(key: unknown): (Entity?, Cleaner?) + local entity = lookup[key] + if not entity then + return nil, nil + end + + return entity, serve_cleaner(key) + end + + local function call(_, key: unknown): (Entity, 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 diff --git a/lib/utilities/tracker.luau b/lib/utilities/tracker.luau new file mode 100644 index 0000000..da8e4d5 --- /dev/null +++ b/lib/utilities/tracker.luau @@ -0,0 +1,273 @@ +--!strict +--!optimize 2 +local command_buffer = require("./command_buffer") +local jecs = require("../../jecs") +type Entity = jecs.Entity +type Id = jecs.Id +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(container: {} & T, index: K): index + local data = container[index] + if not data then + data = {} + container[index] = data + end + return data +end + +local function insert_unique(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): 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(entity: Entity, component: Id) + world:add(entity, component) + end + + function set(entity: Entity, component: Id, data: T) + world:set(entity, component, data) + end + + function remove(entity: Entity, component: Id) + 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 diff --git a/lib/world.luau b/lib/world.luau deleted file mode 100644 index 3e7a183..0000000 --- a/lib/world.luau +++ /dev/null @@ -1,29 +0,0 @@ ---!strict ---!optimize 2 -local jecs = require("../jecs") - -local WORLD: jecs.World - -local listeners: { (jecs.World) -> () } = {} - -local function get(): jecs.World - return WORLD -end - -local function set(world: jecs.World) - WORLD = world - - for _, fn in listeners do - fn(world) - end -end - -local function on_set(fn: (jecs.World) -> ()) - table.insert(listeners, fn) -end - -return { - get = get, - set = set, - on_set = on_set, -} diff --git a/luau_lsp_settings.json b/luau_lsp_settings.json deleted file mode 100644 index 613b218..0000000 --- a/luau_lsp_settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "luau-lsp.fflags.override": { - "LuauTinyControlFlowAnalysis": "true" - }, - "luau-lsp.require.mode": "relativeToFile", - "luau-lsp.require.fileAliases": { - "@jecs": "Packages/jecs" - }, - "luau-lsp.platform.type": "roblox" -} diff --git a/pesde.toml b/pesde.toml index 8b04f9e..285d3d2 100644 --- a/pesde.toml +++ b/pesde.toml @@ -1,17 +1,18 @@ -name = "mark_marks/jecs_utils" -version = "0.1.7-rc.1" -description = "A set of utilities for jecs" -authors = ["marked/Mark-Marks"] -repository = "https://github.com/mark-marks/jecs-utils" +name = "marked/hammer" +version = "0.2.0" +description = "A set of utilities for Jecs" +authors = ["marked"] +repository = "https://git.devmarked.win/marked/hammer" license = "MIT" -includes = ["lib", "LICENSE", "pesde.toml", "README.md", "jecs.luau"] +includes = ["lib", "lib/**", "LICENSE", "pesde.toml", "README.md", "jecs.luau"] [target] environment = "luau" lib = "lib/init.luau" [indices] -default = "https://github.com/daimond113/pesde-index" +default = "https://github.com/pesde-pkg/index" [dependencies] -jecs = { name = "mark_marks/jecs_pesde", version = "^0.4.0" } +# `marked/jecs@0.5.5` was yanked due to some issues, this is the equivalent +jecs = { name = "marked/jecs_nightly", version = "=0.5.5-nightly.20250312T202956Z" } diff --git a/rokit.toml b/rokit.toml index 1c56c74..34f2d48 100644 --- a/rokit.toml +++ b/rokit.toml @@ -5,11 +5,9 @@ [tools] wally = "upliftgames/wally@0.3.2" -rojo = "rojo-rbx/rojo@7.4.4" -lune = "lune-org/lune@0.8.9" -selene = "kampfkarren/selene@0.27.1" -luau-lsp = "johnnymorganz/luau-lsp@1.36.0" -stylua = "johnnymorganz/stylua@0.20.0" -wally-package-types = "johnnymorganz/wally-package-types@1.3.2" -darklua = "seaofvoices/darklua@0.13.1" -pesde = "daimond113/pesde@0.5.0-rc.16" +rojo = "rojo-rbx/rojo@7.5.1" +lune = "lune-org/lune@0.9.2" +selene = "kampfkarren/selene@0.28.0" +luau-lsp = "johnnymorganz/luau-lsp@1.45.0" +stylua = "johnnymorganz/stylua@2.1.0" +pesde = "daimond113/pesde@0.6.2+registry.0.2.2" diff --git a/selene.toml b/selene.toml index d0a4c7f..9afc306 100644 --- a/selene.toml +++ b/selene.toml @@ -1,2 +1 @@ std = "selene_definitions" -exclude = ["lib/jecs.luau"] diff --git a/stylua.toml b/stylua.toml index e61530e..920827f 100644 --- a/stylua.toml +++ b/stylua.toml @@ -3,7 +3,7 @@ line_endings = "Unix" indent_type = "Spaces" indent_width = 4 quote_style = "AutoPreferDouble" -call_parentheses = "Always" +call_parentheses = "Input" collapse_simple_statement = "Never" [sort_requires] diff --git a/test/signal.luau b/test/signal.luau index b508fae..e90e5a0 100644 --- a/test/signal.luau +++ b/test/signal.luau @@ -3,19 +3,19 @@ -- https://github.com/red-blox/Util/blob/main/libs/Signal/Signal.luau -- adapted to work in pure luau -type node = { - next: node?, +type Node = { + next: Node?, callback: (T...) -> (), } -export type signal = { - root: node?, +export type Signal = { + root: Node?, - connect: (self: signal, Callback: (T...) -> ()) -> () -> (), - wait: (self: signal) -> T..., - once: (self: signal, Callback: (T...) -> ()) -> () -> (), - fire: (self: signal, T...) -> (), - disconnect_all: (self: signal) -> (), + connect: (self: Signal, Callback: (T...) -> ()) -> () -> (), + wait: (self: Signal) -> T..., + once: (self: Signal, Callback: (T...) -> ()) -> () -> (), + fire: (self: Signal, T...) -> (), + disconnect_all: (self: Signal) -> (), } 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(self: signal, Node: node) +local function disconnect(self: Signal, Node: Node) if self.root == Node then self.root = Node.next else @@ -40,7 +40,7 @@ local function disconnect(self: signal, Node: node) end end -function Signal.connect(self: signal, Callback: (T...) -> ()): () -> () +function Signal.connect(self: Signal, Callback: (T...) -> ()): () -> () local node = { next = self.root, callback = Callback, @@ -53,30 +53,30 @@ function Signal.connect(self: signal, Callback: (T...) -> ()): () -> end end -function Signal.wait(self: signal): T... +function Signal.wait(self: Signal): T... local Thread = coroutine.running() local Disconnect Disconnect = self:connect(function(...) - Disconnect() + (Disconnect :: any)() coroutine.resume(Thread, ...) end) return coroutine.yield() end -function Signal.once(self: signal, Callback: (T...) -> ()): () -> () +function Signal.once(self: Signal, Callback: (T...) -> ()): () -> () local Disconnect Disconnect = self:connect(function(...) - Disconnect() + (Disconnect :: any)() Callback(...) end) return Disconnect end -function Signal.fire(self: signal, ...: T...) +function Signal.fire(self: Signal, ...: T...) local Current = self.root while Current do @@ -85,11 +85,11 @@ function Signal.fire(self: signal, ...: T...) end end -function Signal.disconnect_all(self: signal) +function Signal.disconnect_all(self: Signal) self.root = nil end -return function(): signal +return function(): Signal return setmetatable({ root = nil, }, Signal) :: any diff --git a/test/tests.luau b/test/tests.luau index d8c9500..b29a6d8 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,18 +1,15 @@ --!strict -- stylua: ignore start +local hammer = require("../lib") local jecs = require("@pkg/jecs") -local jecs_utils = require("@lib/init") local testkit = require("./testkit") -type entity = jecs.Entity +type Entity = jecs.Entity -local collect = jecs_utils.collect -local handle = jecs_utils.handle -local replicator = jecs_utils.replicator -local ref = jecs_utils.ref -local ref_search = ref.search -local command_buffer = jecs_utils.command_buffer -local spawner = jecs_utils.spawner +local collect = hammer.collect +local make_tracker = hammer.tracker +local make_ref = hammer.ref +local make_command_buffer = hammer.command_buffer local signal = require("./signal") @@ -20,9 +17,9 @@ local BENCH, START = testkit.benchmark() local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test() -TEST("jecs_utils.collect()", function() +TEST("hammer.collect()", function() do CASE "collects" - local sig: signal.signal = signal() + local sig: signal.Signal = signal() local flush = collect(sig) local should = {} @@ -38,169 +35,152 @@ TEST("jecs_utils.collect()", function() end end) -TEST("jecs_utils.handle()", function() - do CASE "has" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local entity = world:entity() - local tag = world:entity() - - world:add(entity, tag) - CHECK(handle(entity):has(tag)) - end - - do CASE "get" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local entity = world:entity() - local component = world:component() - - world:set(entity, component, 50) - CHECK(handle(entity):get(component) == 50) - end - - do CASE "add" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local entity = world:entity() - local tag = world:entity() - - handle(entity):add(tag) - CHECK(world:has(entity, tag)) - end - - do CASE "set" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local entity = world:entity() - local component = world:component() - - handle(entity):set(component, 50) - CHECK(world:get(entity, component) == 50) - end - - do CASE "remove" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local entity = world:entity() - local component = world:component() - - handle(entity):set(component, 50) - CHECK(world:get(entity, component) == 50) - end - - do CASE "delete" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local entity = world:entity() - handle(entity):delete() - CHECK(not world:contains(entity)) - end -end) - -TEST("jecs_utils.ref()", function() +TEST("hammer.ref()", function() do CASE "set_ref" local world = jecs.World.new() - jecs_utils.initialize(world) + local ref = make_ref(world, true) - local a: number = ref(1234):id() - local b: number = ref(1234):id() + local a = ref(1234) + local b = ref(1234) CHECK(a == b) end - do CASE "search" + do CASE "find" local world = jecs.World.new() - jecs_utils.initialize(world) + local ref = make_ref(world, true) - local a: number = ref(1234):id() - local b = ref_search(1234) - assert(b) -- give me the type refinements... - CHECK(a == b:id() :: number) + local a = ref(1234) + local b = ref.find(1234) + CHECK(a == b) end - do CASE "clearer" + do CASE "cleaner" local world = jecs.World.new() - jecs_utils.initialize(world) + local ref = make_ref(world, true) - local a, a_clear = ref(1234); - (a_clear :: any)() + local a, clean = ref(1234) + clean() local b = ref(1234) - CHECK(b:id() :: number ~= a:id() :: number) + CHECK(b ~= a) + end + + do CASE "caching" + local world = jecs.World.new() + local ref_a = make_ref(world) + local ref_b = make_ref(world) + CHECK(ref_a == ref_b) + local ref_c = make_ref(world, true) + CHECK(ref_c ~= ref_a and ref_c ~= ref_b) end end) -TEST("jecs_utils.replicator()", function() - do CASE "propagates difference" +-- TODO! write extensive tests for state operation cleaning +TEST("hammer.tracker()", function() + do CASE "snapshot" local world = jecs.World.new() local tag = world:entity() - local component: jecs.Entity = world:component() + local component = world:component() :: Entity local entity1 = world:entity() local entity2 = world:entity() - jecs_utils.initialize(world) - local rep = replicator(component, tag) + local tracker = make_tracker(world, component, tag) world:add(entity1, tag) world:set(entity2, component, 50) - local difference: jecs_utils.changes = rep.calculate_difference() :: any - CHECK(difference ~= nil) + local snapshot = tracker.snapshot() + CHECK(snapshot ~= nil) + assert(snapshot) -- Refinements local world2 = jecs.World.new() - local component2: jecs.Entity = world2:component() + local component2 = world2:component() :: Entity local tag2 = world2:entity() - jecs_utils.initialize(world2) - local rep2 = replicator(component2, tag2) + local tracker2 = make_tracker(world2, component2, tag2) - rep2.apply_difference(difference) + tracker2.apply(snapshot) - CHECK(ref(`replicated-{entity1}`):has(tag2)) - CHECK(ref(`replicated-{entity2}`):get(component2) == 50) + CHECK(world:has(entity1, tag2)) + CHECK(world:get(entity2, component2) == 50) end - do CASE "propagates full data" + do CASE "state" local world = jecs.World.new() local tag = world:entity() - local component: jecs.Entity = world:component() + local component = world:component() :: Entity local entity1 = world:entity() local entity2 = world:entity() - jecs_utils.initialize(world) - local rep = replicator(component, tag) + local tracker = make_tracker(world, component, tag) world:add(entity1, tag) world:set(entity2, component, 50) - local full_data = rep.get_full_data() - CHECK(full_data ~= nil) + local state = tracker.state() + CHECK(state ~= nil) local world2 = jecs.World.new() - local component2: jecs.Entity = world2:component() + local component2 = world2:component() :: Entity local tag2 = world2:entity() - jecs_utils.initialize(world2) - local rep2 = replicator(component2, tag2) + local tracker2 = make_tracker(world2, component2, tag2) - rep2.apply_difference(full_data) + tracker2.apply(state) - CHECK(ref(`replicated-{entity1}`):has(tag2)) - CHECK(ref(`replicated-{entity2}`):get(component2) == 50) + CHECK(world:has(entity1, tag2)) + CHECK(world:get(entity2, component2) == 50) + end + + do CASE "simplifying" + local world = jecs.World.new() + local component = world:component() :: Entity + + local entity = world:entity() + local tracker = make_tracker(world, component) + + world:add(entity, component) + do + local state = tracker.state() + CHECK(table.find(state.added[component :: any], entity)) + end + world:set(entity, component, 50) + do + local state = tracker.state() + CHECK(not table.find(state.added[component :: any], entity)) + CHECK(state.set[component :: any][entity :: any] == 50) + end + world:remove(entity, component) + do + local state = tracker.state() + CHECK(state.set[component :: any][entity :: any] == nil) + CHECK(table.find(state.removed[component :: any], entity)) + end + world:add(entity, component) + do + local state = tracker.state() + CHECK(not table.find(state.removed[component :: any], entity)) + end + world:remove(entity, component) + do + local state = tracker.state() + CHECK(state.set[component :: any][entity :: any] == nil) + CHECK(table.find(state.removed[component :: any], entity)) + end + world:set(entity, component, 50) + do + local state = tracker.state() + CHECK(not table.find(state.removed[component :: any], entity)) + CHECK(state.set[component :: any][entity :: any] == 50) + end end end) -TEST("jecs_utils.command_buffer", function() +TEST("hammer.command_buffer()", function() do CASE "add" local world = jecs.World.new() - jecs_utils.initialize(world) + local command_buffer = make_command_buffer(world) local tag = world:entity() local entity = world:entity() @@ -215,7 +195,7 @@ TEST("jecs_utils.command_buffer", function() do CASE "set" local world = jecs.World.new() - jecs_utils.initialize(world) + local command_buffer = make_command_buffer(world) local component = world:component() local entity = world:entity() @@ -230,7 +210,7 @@ TEST("jecs_utils.command_buffer", function() do CASE "remove" local world = jecs.World.new() - jecs_utils.initialize(world) + local command_buffer = make_command_buffer(world) local component = world:component() local entity = world:entity() @@ -246,7 +226,7 @@ TEST("jecs_utils.command_buffer", function() do CASE "delete" local world = jecs.World.new() - jecs_utils.initialize(world) + local command_buffer = make_command_buffer(world) local entity = world:entity() command_buffer.delete(entity) @@ -255,51 +235,32 @@ TEST("jecs_utils.command_buffer", function() CHECK(not world:contains(entity)) end -end) -TEST("jecs_utils.spawner()", function() - do CASE "spawn" + do CASE "peek" local world = jecs.World.new() - jecs_utils.initialize(world) + local command_buffer = make_command_buffer(world) - local c1: entity = world:component() - local c2: entity = world:component() - local c3: entity<{}> = world:component() + local tag1 = world:entity() + local entity1 = world:entity() + command_buffer.add(entity1, tag1) - local t1 = world:entity() + local component1 = world:component() + local entity2 = world:entity() + command_buffer.set(entity2, component1, 50) - local entity_spawner = spawner(c1, c2, c3, t1) + local tag2 = world:component() + local entity3 = world:entity() + command_buffer.remove(entity3, tag2) - local tbl = {} + local entity4 = world:component() + command_buffer.delete(entity4) - local idx = entity_spawner.spawn(1234, "abcdef", tbl) - CHECK(world:contains(idx)) - CHECK(world:get(idx, c1) == 1234) - CHECK(world:get(idx, c2) == "abcdef") - CHECK(world:get(idx, c3) == tbl) - CHECK(world:has(idx, t1)) - end - - do CASE "spawn_with_handle" - local world = jecs.World.new() - jecs_utils.initialize(world) - - local c1: entity = world:component() - local c2: entity = world:component() - local c3: entity<{}> = world:component() - - local t1 = world:entity() - - local entity_spawner = spawner(c1, c2, c3, t1) - - local tbl = {} - - local ent = entity_spawner.spawn_with_handle(1234, "abcdef", tbl) - CHECK(world:contains(ent:id())) - CHECK(ent:get(c1) == 1234) - CHECK(ent:get(c2) == "abcdef") - CHECK(ent:get(c3) == tbl) - CHECK(ent:has(t1)) + local commands = command_buffer.peek() + CHECK(table.find(commands.add[tag1], entity1)) + CHECK(commands.set[component1][entity2] == 50) + CHECK(table.find(commands.remove[tag2], entity3)) + CHECK(table.find(commands.delete, entity4)) + CHECK(commands.deletion_lookup[entity4] == true) end end) diff --git a/wally.toml b/wally.toml index 1b2bbfe..16442cb 100644 --- a/wally.toml +++ b/wally.toml @@ -1,18 +1,18 @@ [package] -name = "mark-marks/jecs-utils" -version = "0.1.7-rc.1" +name = "mark-marks/hammer" +version = "0.2.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT" exclude = ["**"] include = [ "default.project.json", - "dist", - "dist/**", + "lib", + "lib/**", "LICENSE", "wally.toml", "README.md", ] [dependencies] -jecs = "ukendio/jecs@0.4.0" +jecs = "ukendio/jecs@0.5.5"

- ) -> spawner) - & (( - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id, - id