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 51% rename from .github/workflows/release.yml rename to .forgejo/workflows/release.yml index 02b87f9..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 }} @@ -73,7 +76,7 @@ jobs: run: wally publish - name: Pesde Login - run: pesde auth login --token "Bearer ${{ secrets.WALLY_AUTH_TOKEN }}" + run: pesde auth login --token "${{ secrets.PESDE_AUTH_TOKEN }}" - name: Pesde Publish - run: pesde publish + run: pesde publish -y diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0389aa6..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,85 +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: 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/.gitignore b/.gitignore index cdd52d9..b79fda6 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,9 @@ dist/ # Wally files DevPackages Packages +luau_packages wally.lock +pesde.lock WallyPatches # Typescript @@ -55,7 +57,6 @@ WallyPatches roblox.toml sourcemap.json globalTypes.d.luau -# Used for testing, unfortunately we can't just install it with wally and expect it to work in vanilla luau -jecs.luau +jecs_src.luau # Stores Github PAT .env diff --git a/.luaurc b/.luaurc index 332de00..b88f9c7 100644 --- a/.luaurc +++ b/.luaurc @@ -1,8 +1,7 @@ { "languageMode": "strict", "aliases": { - "jecs_utils": "lib", - "testkit": "test/testkit", - "jecs": "jecs" + "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 2d23e65..0000000 --- a/.lune/download-jecs.luau +++ /dev/null @@ -1,67 +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.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 6482f28..0a07aaf 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,52 +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", - "fileAliases": { - "@jecs_utils": "lib", - "@testkit": "test/testkit", - "@jecs": "Packages/jecs" - }, - "directoryAliases": { - "@lune": ".lune/.lune-defs/" - } + "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 0000000..bc9f48a Binary files /dev/null and b/assets/hammer-logo.png differ 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 7496e83..0000000 --- a/dev.project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "dev", - "tree": { - "$className": "DataModel", - - "ReplicatedStorage": { - "Packages": { - "$className": "Folder", - "$path": "Packages", - "jecs_utils": { - "$path": "lib" - } - } - } - } -} diff --git a/jecs.luau b/jecs.luau new file mode 100644 index 0000000..86e5a2d --- /dev/null +++ b/jecs.luau @@ -0,0 +1,13 @@ +--!strict +--!native +--!optimize 2 +-- pesde adds dependencies by including a folder with them in the package +-- wally just adds a file for each dependency +-- this is here to mitigate that +local jecs = require("./luau_packages/jecs") +export type Archetype = jecs.Archetype +export type Id = jecs.Id +export type Pair = jecs.Pair +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 d4ee535..0000000 --- a/lib/handle.luau +++ /dev/null @@ -1,78 +0,0 @@ ---!strict ---!optimize 2 -local jecs = require("../jecs") -type entity = jecs.Entity -type id = entity | jecs.Pair - -local world = require("./world").get - -type interface = { - __index: interface, - - new: (entity: entity) -> handle, - - --- Checks if the entity has all of the given components - has: (self: handle, ...id) -> boolean, - --- Retrieves the value of up to 4 components. These values may be nil. - get: ((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 28f60c2..285d3d2 100644 --- a/pesde.toml +++ b/pesde.toml @@ -1,17 +1,18 @@ -name = "mark_marks/jecs_utils" -version = "0.1.6" -description = "A set of utilities for jecs" -authors = ["marked/Mark-Marks"] -repository = "https://github.com/mark-marks/jecs-utils" +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" - -include = ["src", "src/**", "LICENSE", "pesde.toml", "README.md"] +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 = { repo = "https://git.devmarked.win/marked/jecs-pesde", rev = "main" } +# `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 9f1de54..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.8" -selene = "kampfkarren/selene@0.27.1" -luau-lsp = "johnnymorganz/luau-lsp@1.32.4" -stylua = "johnnymorganz/stylua@0.20.0" -wally-package-types = "johnnymorganz/wally-package-types@1.3.2" -darklua = "seaofvoices/darklua@0.13.1" -pesde = "daimond113/pesde@0.5.0-rc.8" +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 3ea84b9..b29a6d8 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,18 +1,15 @@ --!strict -- stylua: ignore start -local jecs = require("@jecs") -local jecs_utils = require("@jecs_utils") -local testkit = require("@testkit") +local hammer = require("../lib") +local jecs = require("@pkg/jecs") +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 a8f4a38..16442cb 100644 --- a/wally.toml +++ b/wally.toml @@ -1,18 +1,18 @@ [package] -name = "mark-marks/jecs-utils" -version = "0.1.6" +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.3.2" +jecs = "ukendio/jecs@0.5.5"

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