Initial push
Some checks are pending
CI / Analyze (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Styling (push) Waiting to run

This commit is contained in:
marked 2024-11-11 19:35:24 +01:00
parent 5a9167e2f5
commit 8ee75b96ed
51 changed files with 4437 additions and 2 deletions

25
.darklua.json Normal file
View file

@ -0,0 +1,25 @@
{
"process": [
{
"rule": "convert_require",
"current": {
"name": "path",
"sources": {
"@pkg": "roblox_packages/",
"@net": "src/net/",
"@server": "src/server/",
"@services": "src/server/services/",
"@srv_systems": "src/server/services/systems/",
"@client": "src/client/",
"@controllers": "src/client/controllers/",
"@clt_systems": "src/client/controllers/systems/",
"@shared": "src/shared/"
}
},
"target": {
"name": "roblox",
"rojo_sourcemap": "sourcemap.json"
}
}
]
}

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

@ -0,0 +1,58 @@
name: CI
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: marked/setup-rokit@v0.1.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Pesde packages
run: lune run scripts/install-packages.luau
- name: Analyze
run: lune run scripts/analyze.luau
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rokit
uses: marked/setup-rokit@v0.1.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Lint
run: |
selene src/
style:
name: Styling
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check code style
uses: marked/stylua-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: v0.20.0
args: --check src/

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
sourcemap.json
roblox_packages/
**/*.rbx[lm]*
roblox.yml
roblox.yaml
roblox.toml
globalTypes.d.luau
**/sourcemap.json
dist/

3
.luaurc Normal file
View file

@ -0,0 +1,3 @@
{
"languageMode": "strict"
}

View file

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

289
.lune/.lune-defs/fs.luau Normal file
View file

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

123
.lune/.lune-defs/luau.luau Normal file
View file

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

321
.lune/.lune-defs/net.luau Normal file
View file

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

View file

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

218
.lune/.lune-defs/regex.luau Normal file
View file

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

View file

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

200
.lune/.lune-defs/serde.luau Normal file
View file

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

161
.lune/.lune-defs/stdio.luau Normal file
View file

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

View file

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

11
.lune/analyze.luau Normal file
View file

@ -0,0 +1,11 @@
--!strict
local spawn = require("util/spawn")
spawn.start("lune run install-packages")
spawn.start("rojo sourcemap default.project.json -o sourcemap.json")
spawn.start(
"curl https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/refs/heads/main/scripts/globalTypes.RobloxScriptSecurity.d.luau -o globalTypes.d.luau"
)
spawn.start(
"luau-lsp analyze --definitions=globalTypes.d.luau --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore **/_Index/** --ignore **/.pesde/** --ignore src/client.luau --ignore src/server.luau --ignore src/types.luau src/"
)

7
.lune/build.luau Normal file
View file

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

6
.lune/check.luau Normal file
View file

@ -0,0 +1,6 @@
--!strict
local spawn = require("util/spawn")
spawn.start("lune run analyze")
spawn.start("stylua src/")
spawn.start("selene src/")

58
.lune/dev.luau Normal file
View file

@ -0,0 +1,58 @@
--!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")
task.spawn(watch, "pesde.toml", function()
spawn.spawn("lune run install-packages")
end, false)
spawn.spawn("rojo sourcemap default.project.json -o sourcemap.json --watch")
spawn.spawn("darklua process --config .darklua.json --watch src/ dist/", { env = { ROBLOX_DEV = "true" } })
spawn.spawn("blink net.blink --watch")
spawn.spawn("rojo serve build.project.json")
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

View file

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

39
.lune/util/spawn.luau Normal file
View file

@ -0,0 +1,39 @@
--!strict
local process = require("@lune/process")
local task = require("@lune/task")
--- Start a process with the given command and options
--- ```luau
--- spawn.start("lune run test")
--- ```
--- @param cmd string
--- @param options process.SpawnOptions?
--- @return process.SpawnResult
local function start_process(cmd: string, options: process.SpawnOptions?): process.SpawnResult
local arguments = string.split(cmd, " ")
local command = arguments[1]
table.remove(arguments, 1)
local opts: process.SpawnOptions = options ~= nil and options or {}
opts.stdio = opts.stdio ~= nil and opts.stdio or "forward"
return process.spawn(command, arguments, opts)
end
--- `task.spawn` a process with the given command and options
--- ```luau
--- spawn.spawn("lune run test") -- process now runs in the background!
--- ```
--- @param cmd string
--- @param options process.SpawnOptions?
--- @return process.SpawnResult
local function spawn_process(cmd: string, options: process.SpawnOptions?)
task.spawn(start_process, cmd, options)
end
local spawn = {
start = start_process,
spawn = spawn_process,
}
return spawn

46
.lune/util/watch.luau Normal file
View file

@ -0,0 +1,46 @@
--!strict
local fs = require("@lune/fs")
local task = require("@lune/task")
local function watch(path: string, on_change: (path: string, contents: string) -> (), run_initially: boolean?)
local initial_metadata = fs.metadata(path)
if not initial_metadata.exists then
return
end
if run_initially then
local initial_contents = fs.readFile(path)
local initial_success, why = pcall(on_change :: any, path, initial_contents) -- :: any because otherwise it shits itself and the type doesn't give (boolean, string)??
if not initial_success then
warn(`There was an error while trying to start the watcher thread:\n{why}`)
return
end
end
local last_modification = initial_metadata.modifiedAt
while true do
local metadata = fs.metadata(path)
if not metadata.exists then
continue
end
if metadata.modifiedAt == last_modification then
continue
end
last_modification = metadata.modifiedAt
local contents = fs.readFile(path)
local success, err = pcall(on_change :: any, path, contents)
if not success then
warn(err)
end
task.wait(1)
end
end
return watch

View file

@ -0,0 +1,4 @@
local process = require("@lune/process")
local home_dir = if process.os == "windows" then process.env.userprofile else process.env.HOME
require(home_dir .. "/.pesde/scripts/lune/rojo/roblox_sync_config_generator.luau")

View file

@ -0,0 +1,4 @@
local process = require("@lune/process")
local home_dir = if process.os == "windows" then process.env.userprofile else process.env.HOME
require(home_dir .. "/.pesde/scripts/lune/rojo/sourcemap_generator.luau")

3
.styluaignore Normal file
View file

@ -0,0 +1,3 @@
src/net/server.luau
src/net/client.luau
src/net/types.luau

62
.zed/settings.json Normal file
View file

@ -0,0 +1,62 @@
{
"lsp": {
"luau-lsp": {
"settings": {
"luau-lsp": {
"completion": {
"imports": {
"enabled": true,
"separateGroupsWithLine": true,
"suggestServices": true,
"suggestRequires": false
}
},
"require": {
"mode": "relativeToFile",
"directoryAliases": {
"@lune/": ".lune/.lune-defs/",
"@pkg/": "roblox_packages/",
"@net/": "src/net/",
"@server/": "src/server/",
"@services/": "src/server/services/",
"@srv_systems/": "src/server/services/systems/",
"@client/": "src/client/",
"@controllers/": "src/client/controllers/",
"@clt_systems/": "src/client/controllers/systems/",
"@shared/": "src/shared/"
}
},
"inlayHints": {
"parameterNames": "all"
}
},
"ext": {
"roblox": {
"enabled": true,
"security_level": "roblox_script"
},
"fflags": {
"enable_by_default": false,
"sync": true
},
"binary": {
"ignore_system_version": false
}
}
}
}
},
"languages": {
"Luau": {
"formatter": {
"external": {
"command": "stylua",
"arguments": ["-"]
}
}
}
},
"file_types": {
"Luau": ["lua"]
}
}

View file

@ -1,4 +1,8 @@
# color-pads
# roblox-project-template
This is a rewrite of Color Pads using jecs instead of ECR.
A template for Roblox projects using rojo, darklua, jecs and sapphire.
## Workflows
These workflows are made for [my forgejo instance](https://git.devmarked.win/). In order to make them work on GitHub, you have to change the used actions (such as `marked/setup-rokit`) to match the ones available on GitHub (for example `CompeyDev/setup-rokit`).\
You also have to rename `.forgejo` to `.github`, although that should be pretty self explanatory.

27
build.project.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "roblox-project-template",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$path": "dist/server"
},
"ReplicatedStorage": {
"$path": "dist/shared",
"net": {
"$path": "dist/net"
},
"client": {
"$path": "dist/client"
},
"packages": {
"$path": "roblox_packages"
}
}
}
}

27
default.project.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "roblox-project-template",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$path": "src/server"
},
"ReplicatedStorage": {
"$path": "src/shared",
"net": {
"$path": "src/net"
},
"client": {
"$path": "src/client"
},
"packages": {
"$path": "roblox_packages"
}
}
}
}

17
luau_lsp_settings.json Normal file
View file

@ -0,0 +1,17 @@
{
"luau-lsp.platform.type": "roblox",
"luau-lsp.sourcemap.rojoProjectFile": "default.project.json",
"luau-lsp.require.mode": "relativeToFile",
"luau-lsp.require.directoryAliases": {
"@lune/": ".lune/.lune-defs/",
"@pkg/": "roblox_packages/",
"@net/": "src/net/",
"@server/": "src/server/",
"@services/": "src/server/services/",
"@srv_systems/": "src/server/services/systems/",
"@client/": "src/client/",
"@controllers/": "src/client/controllers/",
"@clt_systems/": "src/client/controllers/systems/",
"@shared/": "src/shared/"
}
}

11
net.blink Normal file
View file

@ -0,0 +1,11 @@
option Casing = Snake
option ServerOutput = "src/net/server.luau"
option ClientOutput = "src/net/client.luau"
option TypesOutput = "src/net/types.luau"
event replicate_data {
from: Server,
type: Reliable,
call: SingleSync,
data: buffer,
}

335
pesde.lock Normal file
View file

@ -0,0 +1,335 @@
name = "marked/roblox_project_template"
version = "0.1.0"
target = "roblox"
[graph."wally#alicesaidhi/jabby"."0.2.0-rc.3 roblox"]
ty = "standard"
[graph."wally#alicesaidhi/jabby"."0.2.0-rc.3 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#alicesaidhi/jabby"."0.2.0-rc.3 roblox".dependencies]
"wally#alicesaidhi/pebble" = ["0.1.0-rc.7 roblox", "pebble"]
"wally#centau/vide" = ["0.3.1 roblox", "vide"]
"wally#corecii/greentea" = ["0.4.10 roblox", "t"]
"wally#ukendio/jecs" = ["0.3.2 roblox", "jecs"]
[graph."wally#alicesaidhi/jabby"."0.2.0-rc.3 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#alicesaidhi/jabby"
version = "0.2.0-rc.3"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#alicesaidhi/jabby"."0.2.0-rc.3 roblox".pkg_ref.dependencies]
jecs = [{ wally = "wally#ukendio/jecs", version = ">=0.3.0-rc.2, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
pebble = [{ wally = "wally#alicesaidhi/pebble", version = ">=0.1.0-rc.7, <0.2.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
t = [{ wally = "wally#corecii/greentea", version = ">=0.4.7, <0.5.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
vide = [{ wally = "wally#centau/vide", version = ">=0.3.1, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#alicesaidhi/pebble"."0.1.0-rc.7 roblox"]
ty = "standard"
[graph."wally#alicesaidhi/pebble"."0.1.0-rc.7 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#alicesaidhi/pebble"."0.1.0-rc.7 roblox".dependencies]
"wally#centau/vide" = ["0.3.1 roblox", "vide"]
[graph."wally#alicesaidhi/pebble"."0.1.0-rc.7 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#alicesaidhi/pebble"
version = "0.1.0-rc.7"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#alicesaidhi/pebble"."0.1.0-rc.7 roblox".pkg_ref.dependencies]
vide = [{ wally = "wally#centau/vide", version = ">=0.3.1, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#centau/vide"."0.3.1 roblox"]
ty = "standard"
[graph."wally#centau/vide"."0.3.1 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#centau/vide"."0.3.1 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#centau/vide"
version = "0.3.1"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#corecii/greentea"."0.4.10 roblox"]
ty = "standard"
[graph."wally#corecii/greentea"."0.4.10 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#corecii/greentea"."0.4.10 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#corecii/greentea"
version = "0.4.10"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#corecii/greentea"."0.4.10 roblox".pkg_ref.dependencies]
Jest = [{ wally = "wally#jsdotlua/jest", version = ">=3.6.1-rc.2, <4.0.0", index = "https://github.com/UpliftGames/wally-index" }, "dev"]
JestGlobals = [{ wally = "wally#jsdotlua/jest-globals", version = ">=3.6.1-rc.2, <4.0.0", index = "https://github.com/UpliftGames/wally-index" }, "dev"]
RegExp = [{ wally = "wally#jsdotlua/luau-regexp", version = ">=0.2.1, <0.3.0", index = "https://github.com/UpliftGames/wally-index" }, "dev"]
_jest-roblox-shared = [{ wally = "wally#jsdotlua/jest-roblox-shared", version = ">=3.6.1-rc.2, <4.0.0", index = "https://github.com/UpliftGames/wally-index" }, "dev"]
[graph."wally#ffrostflame/keyform"."0.2.2 roblox"]
ty = "standard"
[graph."wally#ffrostflame/keyform"."0.2.2 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#ffrostflame/keyform"."0.2.2 roblox".dependencies]
"wally#ffrostflame/luausignal" = ["0.1.3 roblox", "Signal"]
"wally#ffrostflame/tablekit" = ["0.2.4 roblox", "TableKit"]
[graph."wally#ffrostflame/keyform"."0.2.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#ffrostflame/keyform"
version = "0.2.2"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#ffrostflame/keyform"."0.2.2 roblox".pkg_ref.dependencies]
Signal = [{ wally = "wally#ffrostflame/luausignal", version = ">=0.1.3, <0.2.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
TableKit = [{ wally = "wally#ffrostflame/tablekit", version = ">=0.2.2, <0.3.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#ffrostflame/luausignal"."0.1.3 roblox"]
ty = "standard"
[graph."wally#ffrostflame/luausignal"."0.1.3 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#ffrostflame/luausignal"."0.1.3 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#ffrostflame/luausignal"
version = "0.1.3"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#ffrostflame/tablekit"."0.2.4 roblox"]
ty = "standard"
[graph."wally#ffrostflame/tablekit"."0.2.4 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#ffrostflame/tablekit"."0.2.4 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#ffrostflame/tablekit"
version = "0.2.4"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/jecs-utils"."0.1.5 roblox"]
ty = "standard"
[graph."wally#mark-marks/jecs-utils"."0.1.5 roblox".target]
environment = "roblox"
lib = 'dist\init.luau'
build_files = []
[graph."wally#mark-marks/jecs-utils"."0.1.5 roblox".dependencies]
"wally#ukendio/jecs" = ["0.3.2 roblox", "jecs"]
[graph."wally#mark-marks/jecs-utils"."0.1.5 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#mark-marks/jecs-utils"
version = "0.1.5"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/jecs-utils"."0.1.5 roblox".pkg_ref.dependencies]
jecs = [{ wally = "wally#ukendio/jecs", version = ">=0.3.2, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#mark-marks/sapphire"."0.1.3 roblox"]
direct = ["sapphire", { wally = "wally#mark-marks/sapphire", version = "^0.1.3" }]
ty = "standard"
[graph."wally#mark-marks/sapphire"."0.1.3 roblox".target]
environment = "roblox"
lib = 'lib\init.luau'
build_files = []
[graph."wally#mark-marks/sapphire"."0.1.3 roblox".dependencies]
"wally#red-blox/signal" = ["2.0.2 roblox", "signal"]
"wally#red-blox/spawn" = ["1.1.0 roblox", "spawn"]
[graph."wally#mark-marks/sapphire"."0.1.3 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#mark-marks/sapphire"
version = "0.1.3"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/sapphire"."0.1.3 roblox".pkg_ref.dependencies]
signal = [{ wally = "wally#red-blox/signal", version = ">=2.0.2, <3.0.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
spawn = [{ wally = "wally#red-blox/spawn", version = ">=1.1.0, <2.0.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#mark-marks/sapphire-data"."0.1.2 roblox"]
direct = ["sapphire_data", { wally = "wally#mark-marks/sapphire-data", version = "^0.1.2" }]
ty = "standard"
[graph."wally#mark-marks/sapphire-data"."0.1.2 roblox".target]
environment = "roblox"
lib = 'lib\init.luau'
build_files = []
[graph."wally#mark-marks/sapphire-data"."0.1.2 roblox".dependencies]
"wally#ffrostflame/keyform" = ["0.2.2 roblox", "keyform"]
"wally#nezuo/delta-compress" = ["0.1.3 roblox", "delta_compress"]
"wally#red-blox/signal" = ["2.0.2 roblox", "signal"]
[graph."wally#mark-marks/sapphire-data"."0.1.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#mark-marks/sapphire-data"
version = "0.1.2"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/sapphire-data"."0.1.2 roblox".pkg_ref.dependencies]
delta_compress = [{ wally = "wally#nezuo/delta-compress", version = ">=0.1.3, <0.2.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
keyform = [{ wally = "wally#ffrostflame/keyform", version = ">=0.2.2, <0.3.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
signal = [{ wally = "wally#red-blox/signal", version = ">=2.0.2, <3.0.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#mark-marks/sapphire-jecs"."0.2.0-rc.2 roblox"]
direct = ["sapphire_jecs", { wally = "wally#mark-marks/sapphire-jecs", version = "^0.2.0-rc.2" }]
ty = "standard"
[graph."wally#mark-marks/sapphire-jecs"."0.2.0-rc.2 roblox".target]
environment = "roblox"
lib = 'lib\init.luau'
build_files = []
[graph."wally#mark-marks/sapphire-jecs"."0.2.0-rc.2 roblox".dependencies]
"wally#alicesaidhi/jabby" = ["0.2.0-rc.3 roblox", "jabby"]
"wally#mark-marks/jecs-utils" = ["0.1.5 roblox", "jecs_utils"]
"wally#ukendio/jecs" = ["0.3.2 roblox", "jecs"]
[graph."wally#mark-marks/sapphire-jecs"."0.2.0-rc.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#mark-marks/sapphire-jecs"
version = "0.2.0-rc.2"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/sapphire-jecs"."0.2.0-rc.2 roblox".pkg_ref.dependencies]
jabby = [{ wally = "wally#alicesaidhi/jabby", version = ">=0.2.0-rc.2, <0.3.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
jecs = [{ wally = "wally#ukendio/jecs", version = ">=0.3.2, <0.4.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
jecs_utils = [{ wally = "wally#mark-marks/jecs-utils", version = ">=0.1.5, <0.2.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#mark-marks/sapphire-lifecycles"."0.1.2 roblox"]
direct = ["sapphire_lifecycles", { wally = "wally#mark-marks/sapphire-lifecycles", version = "^0.1.2" }]
ty = "standard"
[graph."wally#mark-marks/sapphire-lifecycles"."0.1.2 roblox".target]
environment = "roblox"
lib = 'lib\init.luau'
build_files = []
[graph."wally#mark-marks/sapphire-lifecycles"."0.1.2 roblox".dependencies]
"wally#red-blox/signal" = ["2.0.2 roblox", "signal"]
[graph."wally#mark-marks/sapphire-lifecycles"."0.1.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#mark-marks/sapphire-lifecycles"
version = "0.1.2"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/sapphire-lifecycles"."0.1.2 roblox".pkg_ref.dependencies]
signal = [{ wally = "wally#red-blox/signal", version = ">=2.0.2, <3.0.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#mark-marks/sapphire-logging"."0.1.2 roblox"]
direct = ["sapphire_logging", { wally = "wally#mark-marks/sapphire-logging", version = "^0.1.2" }]
ty = "standard"
[graph."wally#mark-marks/sapphire-logging"."0.1.2 roblox".target]
environment = "roblox"
lib = 'lib\init.luau'
build_files = []
[graph."wally#mark-marks/sapphire-logging"."0.1.2 roblox".dependencies]
"wally#red-blox/signal" = ["2.0.2 roblox", "signal"]
[graph."wally#mark-marks/sapphire-logging"."0.1.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#mark-marks/sapphire-logging"
version = "0.1.2"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#mark-marks/sapphire-logging"."0.1.2 roblox".pkg_ref.dependencies]
signal = [{ wally = "wally#red-blox/signal", version = ">=2.0.2, <3.0.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#nezuo/delta-compress"."0.1.3 roblox"]
ty = "standard"
[graph."wally#nezuo/delta-compress"."0.1.3 roblox".target]
environment = "roblox"
lib = 'src\init.lua'
build_files = []
[graph."wally#nezuo/delta-compress"."0.1.3 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#nezuo/delta-compress"
version = "0.1.3"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#nezuo/delta-compress"."0.1.3 roblox".pkg_ref.dependencies]
Midori = [{ wally = "wally#nezuo/midori", version = ">=0.1.3, <0.2.0", index = "https://github.com/UpliftGames/wally-index" }, "dev"]
RemotePacketSizeCounter = [{ wally = "wally#pysephwasntavailable/remotepacketsizecounter", version = ">=2.4.1, <3.0.0", index = "https://github.com/UpliftGames/wally-index" }, "dev"]
[graph."wally#red-blox/signal"."2.0.2 roblox"]
ty = "standard"
[graph."wally#red-blox/signal"."2.0.2 roblox".target]
environment = "roblox"
lib = "Signal.luau"
build_files = []
[graph."wally#red-blox/signal"."2.0.2 roblox".dependencies]
"wally#red-blox/spawn" = ["1.1.0 roblox", "Spawn"]
[graph."wally#red-blox/signal"."2.0.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#red-blox/signal"
version = "2.0.2"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#red-blox/signal"."2.0.2 roblox".pkg_ref.dependencies]
Spawn = [{ wally = "wally#red-blox/spawn", version = ">=1.0.0, <2.0.0", index = "https://github.com/UpliftGames/wally-index" }, "standard"]
[graph."wally#red-blox/spawn"."1.1.0 roblox"]
ty = "standard"
[graph."wally#red-blox/spawn"."1.1.0 roblox".target]
environment = "roblox"
lib = "Spawn.luau"
build_files = []
[graph."wally#red-blox/spawn"."1.1.0 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#red-blox/spawn"
version = "1.1.0"
index_url = "https://github.com/UpliftGames/wally-index"
[graph."wally#ukendio/jecs"."0.3.2 roblox"]
direct = ["jecs", { wally = "wally#ukendio/jecs", version = "^0.3.2" }]
ty = "standard"
[graph."wally#ukendio/jecs"."0.3.2 roblox".target]
environment = "roblox"
lib = 'src\init.luau'
build_files = []
[graph."wally#ukendio/jecs"."0.3.2 roblox".pkg_ref]
ref_ty = "wally"
wally = "wally#ukendio/jecs"
version = "0.3.2"
index_url = "https://github.com/UpliftGames/wally-index"

26
pesde.toml Normal file
View file

@ -0,0 +1,26 @@
name = "marked/roblox_project_template"
version = "0.1.0"
description = "Template for Roblox projects"
authors = ["marked"]
repository = "https://git.devmarked.win/marked/roblox-project-template"
[target]
environment = "roblox"
[scripts]
roblox_sync_config_generator = ".pesde/roblox_sync_config_generator.luau"
sourcemap_generator = ".pesde/sourcemap_generator.luau"
[indices]
default = "https://github.com/daimond113/pesde-index"
[wally_indices]
default = "https://github.com/UpliftGames/wally-index"
[dependencies]
sapphire = { wally = "wally#mark-marks/sapphire", version = "^0.1.3" }
sapphire_data = { wally = "wally#mark-marks/sapphire-data", version = "^0.1.2" }
sapphire_jecs = { wally = "wally#mark-marks/sapphire-jecs", version = "0.2.0-rc.2" }
sapphire_logging = { wally = "wally#mark-marks/sapphire-logging", version = "^0.1.2" }
sapphire_lifecycles = { wally = "wally#mark-marks/sapphire-lifecycles", version = "^0.1.2" }
jecs = { wally = "wally#ukendio/jecs", version = "^0.3.2" }

14
rokit.toml Normal file
View file

@ -0,0 +1,14 @@
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
# For more information, see https://github.com/rojo-rbx/rokit
# New tools can be added by running `rokit add <tool>` in a terminal.
[tools]
pesde = "daimond113/pesde@0.5.0-rc.7"
stylua = "johnnymorganz/stylua@0.20.0"
lune = "lune-org/lune@0.8.9"
luau-lsp = "johnnymorganz/luau-lsp@1.35.0"
selene = "kampfkarren/selene@0.27.1"
darklua = "seaofvoices/darklua@0.14.0"
blink = "1axen/blink@0.14.15"
rojo = "rojo-rbx/rojo@7.4.4"

2
selene.toml Normal file
View file

@ -0,0 +1,2 @@
std = "selene_definitions"
exclude = ["src/net/client.luau", "src/net/server.luau", "src/net/types.luau"]

6
selene_definitions.yaml Normal file
View file

@ -0,0 +1,6 @@
base: roblox
name: selene_definitions
globals:
require:
args:
- type: string

View file

@ -0,0 +1,15 @@
--!strict
local data = require("@client/data")
local net = require("@net/client")
local function init()
net.replicate_data.on(function(difference)
data.apply_difference(difference)
end)
end
return {
init = init,
-- data is important
priority = math.huge,
}

View file

14
src/client/data.luau Normal file
View file

@ -0,0 +1,14 @@
--!strict
local sapphire_data = require("@pkg/sapphire_data")
export type data = {
coins: number,
}
local template: data = {
coins = 0,
}
return sapphire_data.client({
template = template,
})

View file

@ -0,0 +1,16 @@
--!strict
local sapphire = require("@pkg/sapphire")
local data = require("@client/data")
local ecs = require("@pkg/sapphire_jecs")
local lifecycles = require("@pkg/sapphire_lifecycles")
local controllers = script.Parent.controllers
sapphire()
:register_singletons(controllers)
:register_singletons(controllers.systems)
:use(data)
:use(ecs)
:use(lifecycles)
:start()

10
src/client/player.luau Normal file
View file

@ -0,0 +1,10 @@
--!strict
local Players = game:GetService("Players")
local components = require("@shared/components")
local ecs = require("@pkg/sapphire_jecs")
local player = Players.LocalPlayer
local player_entity = ecs.ref("local-player"):set(components.player, player):id()
return player_entity

265
src/net/client.luau Normal file
View file

@ -0,0 +1,265 @@
--!strict
--!native
--!optimize 2
--!nolint LocalShadow
--#selene: allow(shadowing)
-- File generated by Blink v0.14.15 (https://github.com/1Axen/Blink)
-- This file is not meant to be edited
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Invocations = 0
local SendSize = 64
local SendOffset = 0
local SendCursor = 0
local SendBuffer = buffer.create(64)
local SendInstances = {}
local RecieveCursor = 0
local RecieveBuffer = buffer.create(64)
local RecieveInstances = {}
local RecieveInstanceCursor = 0
type Entry = {
value: any,
next: Entry?
}
type Queue = {
head: Entry?,
tail: Entry?
}
type BufferSave = {
Size: number,
Cursor: number,
Buffer: buffer,
Instances: {Instance}
}
local function Read(Bytes: number)
local Offset = RecieveCursor
RecieveCursor += Bytes
return Offset
end
local function Save(): BufferSave
return {
Size = SendSize,
Cursor = SendCursor,
Buffer = SendBuffer,
Instances = SendInstances
}
end
local function Load(Save: BufferSave?)
if Save then
SendSize = Save.Size
SendCursor = Save.Cursor
SendOffset = Save.Cursor
SendBuffer = Save.Buffer
SendInstances = Save.Instances
return
end
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
SendInstances = {}
end
local function Invoke()
if Invocations == 255 then
Invocations = 0
end
local Invocation = Invocations
Invocations += 1
return Invocation
end
local function Allocate(Bytes: number)
local InUse = (SendCursor + Bytes)
if InUse > SendSize then
--> Avoid resizing the buffer for every write
while InUse > SendSize do
SendSize *= 1.5
end
local Buffer = buffer.create(SendSize)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
SendBuffer = Buffer
end
SendOffset = SendCursor
SendCursor += Bytes
return SendOffset
end
local function CreateQueue(): Queue
return {
head = nil,
tail = nil
}
end
local function Pop(queue: Queue): any
local head = queue.head
if head == nil then
return
end
queue.head = head.next
return head.value
end
local function Push(queue: Queue, value: any)
local entry: Entry = {
value = value,
next = nil
}
if queue.tail ~= nil then
queue.tail.next = entry
end
queue.tail = entry
if queue.head == nil then
queue.head = entry
end
end
local Types = {}
local Calls = table.create(256)
local Events: any = {
Reliable = table.create(256),
Unreliable = table.create(256)
}
local Queue: any = {
Reliable = table.create(256),
Unreliable = table.create(256)
}
Queue.Reliable[0] = table.create(256)
function Types.ReadEVENT_replicate_data(): (buffer)
-- Read BLOCK: 2 bytes
local BLOCK_START = Read(2)
local Length = buffer.readu16(RecieveBuffer, BLOCK_START + 0)
local Value = buffer.create(Length)
buffer.copy(Value, 0, RecieveBuffer, Read(Length), Length)
return Value
end
function Types.WriteEVENT_replicate_data(Value: buffer): ()
-- Allocate BLOCK: 3 bytes
local BLOCK_START = Allocate(3)
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
local Length = buffer.len(Value)
buffer.writeu16(SendBuffer, BLOCK_START + 1, Length)
Allocate(Length)
buffer.copy(SendBuffer, SendOffset, Value, 0, Length)
end
if not RunService:IsRunning() then
local NOOP = function() end
local Returns = table.freeze({
replicate_data = {
on = NOOP
},
})
return Returns :: BLINK_EVENTS_SYMBOL
end
if not RunService:IsClient() then
error("Client network module can only be required from the client.")
end
local Reliable: RemoteEvent = ReplicatedStorage:WaitForChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent
local Unreliable: UnreliableRemoteEvent = ReplicatedStorage:WaitForChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
local function StepReplication()
if SendCursor <= 0 then
return
end
local Buffer = buffer.create(SendCursor)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
Reliable:FireServer(Buffer, SendInstances)
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
table.clear(SendInstances)
end
local Elapsed = 0
RunService.Heartbeat:Connect(function(DeltaTime: number)
Elapsed += DeltaTime
if Elapsed >= (1 / 61) then
Elapsed -= (1 / 61)
StepReplication()
end
end)
Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: {Instance})
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while (RecieveCursor < Size) do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
if Index == 0 then
local Value: buffer = Types.ReadEVENT_replicate_data()
if Events.Reliable[0] ~= nil then
Events.Reliable[0](Value)
else
if #Queue.Reliable[0] > 256 then
warn("[Blink]: Event queue of \"replicate_data\" exceeded 256, did you forget to implement a listener?")
end
table.insert(Queue.Reliable[0], {Value} :: {any})
end
end
end
end)
Unreliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: {Instance})
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while (RecieveCursor < Size) do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
end
end)
local Returns = table.freeze({
step_replication = StepReplication,
replicate_data = {
on = function(Listener: (Value: buffer) -> ()): () -> ()
Events.Reliable[0] = Listener
for Index, Arguments in Queue.Reliable[0] do
Listener(table.unpack(Arguments))
end
Queue.Reliable[0] = {}
return function (): ()
Events.Reliable[0] = nil
end
end
},
})
type BLINK_EVENTS_SYMBOL = typeof(Returns)
return Returns :: BLINK_EVENTS_SYMBOL

304
src/net/server.luau Normal file
View file

@ -0,0 +1,304 @@
--!strict
--!native
--!optimize 2
--!nolint LocalShadow
--#selene: allow(shadowing)
-- File generated by Blink v0.14.15 (https://github.com/1Axen/Blink)
-- This file is not meant to be edited
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Invocations = 0
local SendSize = 64
local SendOffset = 0
local SendCursor = 0
local SendBuffer = buffer.create(64)
local SendInstances = {}
local RecieveCursor = 0
local RecieveBuffer = buffer.create(64)
local RecieveInstances = {}
local RecieveInstanceCursor = 0
type Entry = {
value: any,
next: Entry?
}
type Queue = {
head: Entry?,
tail: Entry?
}
type BufferSave = {
Size: number,
Cursor: number,
Buffer: buffer,
Instances: {Instance}
}
local function Read(Bytes: number)
local Offset = RecieveCursor
RecieveCursor += Bytes
return Offset
end
local function Save(): BufferSave
return {
Size = SendSize,
Cursor = SendCursor,
Buffer = SendBuffer,
Instances = SendInstances
}
end
local function Load(Save: BufferSave?)
if Save then
SendSize = Save.Size
SendCursor = Save.Cursor
SendOffset = Save.Cursor
SendBuffer = Save.Buffer
SendInstances = Save.Instances
return
end
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
SendInstances = {}
end
local function Invoke()
if Invocations == 255 then
Invocations = 0
end
local Invocation = Invocations
Invocations += 1
return Invocation
end
local function Allocate(Bytes: number)
local InUse = (SendCursor + Bytes)
if InUse > SendSize then
--> Avoid resizing the buffer for every write
while InUse > SendSize do
SendSize *= 1.5
end
local Buffer = buffer.create(SendSize)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
SendBuffer = Buffer
end
SendOffset = SendCursor
SendCursor += Bytes
return SendOffset
end
local function CreateQueue(): Queue
return {
head = nil,
tail = nil
}
end
local function Pop(queue: Queue): any
local head = queue.head
if head == nil then
return
end
queue.head = head.next
return head.value
end
local function Push(queue: Queue, value: any)
local entry: Entry = {
value = value,
next = nil
}
if queue.tail ~= nil then
queue.tail.next = entry
end
queue.tail = entry
if queue.head == nil then
queue.head = entry
end
end
local Types = {}
local Calls = table.create(256)
local Events: any = {
Reliable = table.create(256),
Unreliable = table.create(256)
}
local Queue: any = {
Reliable = table.create(256),
Unreliable = table.create(256)
}
function Types.ReadEVENT_replicate_data(): (buffer)
-- Read BLOCK: 2 bytes
local BLOCK_START = Read(2)
local Length = buffer.readu16(RecieveBuffer, BLOCK_START + 0)
local Value = buffer.create(Length)
buffer.copy(Value, 0, RecieveBuffer, Read(Length), Length)
return Value
end
function Types.WriteEVENT_replicate_data(Value: buffer): ()
-- Allocate BLOCK: 3 bytes
local BLOCK_START = Allocate(3)
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
local Length = buffer.len(Value)
buffer.writeu16(SendBuffer, BLOCK_START + 1, Length)
Allocate(Length)
buffer.copy(SendBuffer, SendOffset, Value, 0, Length)
end
if not RunService:IsRunning() then
local NOOP = function() end
local Returns = table.freeze({
replicate_data = {
fire = NOOP,
fire_all = NOOP,
fire_list = NOOP,
fire_except = NOOP
},
})
return Returns :: BLINK_EVENTS_SYMBOL
end
if not RunService:IsServer() then
error("Server network module can only be required from the server.")
end
local Reliable: RemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent
if not Reliable then
local RemoteEvent = Instance.new("RemoteEvent")
RemoteEvent.Name = "BLINK_RELIABLE_REMOTE"
RemoteEvent.Parent = ReplicatedStorage
Reliable = RemoteEvent
end
local Unreliable: UnreliableRemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
if not Unreliable then
local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent")
UnreliableRemoteEvent.Name = "BLINK_UNRELIABLE_REMOTE"
UnreliableRemoteEvent.Parent = ReplicatedStorage
Unreliable = UnreliableRemoteEvent
end
local PlayersMap: {[Player]: BufferSave} = {}
Players.PlayerRemoving:Connect(function(Player)
PlayersMap[Player] = nil
end)
local function StepReplication()
for Player, Send in PlayersMap do
if Send.Cursor <= 0 then
continue
end
local Buffer = buffer.create(Send.Cursor)
buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor)
Reliable:FireClient(Player, Buffer, Send.Instances)
Send.Size = 64
Send.Cursor = 0
Send.Buffer = buffer.create(64)
table.clear(Send.Instances)
end
end
RunService.Heartbeat:Connect(StepReplication)
Reliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: {Instance})
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while (RecieveCursor < Size) do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
end
end)
Unreliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: {Instance})
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while (RecieveCursor < Size) do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
end
end)
local Returns = table.freeze({
step_replication = StepReplication,
replicate_data = {
fire = function(Player: Player, Value: buffer): ()
Load(PlayersMap[Player])
Types.WriteEVENT_replicate_data(Value)
PlayersMap[Player] = Save()
end,
fire_all = function(Value: buffer): ()
Load()
Types.WriteEVENT_replicate_data(Value)
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
for _, Player in Players:GetPlayers() do
Load(PlayersMap[Player])
local Position = Allocate(Size)
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
PlayersMap[Player] = Save()
end
end,
fire_list = function(List: {Player}, Value: buffer): ()
Load()
Types.WriteEVENT_replicate_data(Value)
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
for _, Player in List do
Load(PlayersMap[Player])
local Position = Allocate(Size)
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
PlayersMap[Player] = Save()
end
end,
fire_except = function(Except: Player, Value: buffer): ()
Load()
Types.WriteEVENT_replicate_data(Value)
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
for _, Player in Players:GetPlayers() do
if Player == Except then
continue
end
Load(PlayersMap[Player])
local Position = Allocate(Size)
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
PlayersMap[Player] = Save()
end
end,
},
})
type BLINK_EVENTS_SYMBOL = typeof(Returns)
return Returns :: BLINK_EVENTS_SYMBOL

153
src/net/types.luau Normal file
View file

@ -0,0 +1,153 @@
--!strict
--!native
--!optimize 2
--!nolint LocalShadow
--#selene: allow(shadowing)
-- File generated by Blink v0.14.15 (https://github.com/1Axen/Blink)
-- This file is not meant to be edited
local Invocations = 0
local SendSize = 64
local SendOffset = 0
local SendCursor = 0
local SendBuffer = buffer.create(64)
local SendInstances = {}
local RecieveCursor = 0
local RecieveBuffer = buffer.create(64)
local RecieveInstances = {}
local RecieveInstanceCursor = 0
type Entry = {
value: any,
next: Entry?
}
type Queue = {
head: Entry?,
tail: Entry?
}
type BufferSave = {
Size: number,
Cursor: number,
Buffer: buffer,
Instances: {Instance}
}
local function Read(Bytes: number)
local Offset = RecieveCursor
RecieveCursor += Bytes
return Offset
end
local function Save(): BufferSave
return {
Size = SendSize,
Cursor = SendCursor,
Buffer = SendBuffer,
Instances = SendInstances
}
end
local function Load(Save: BufferSave?)
if Save then
SendSize = Save.Size
SendCursor = Save.Cursor
SendOffset = Save.Cursor
SendBuffer = Save.Buffer
SendInstances = Save.Instances
return
end
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
SendInstances = {}
end
local function Invoke()
if Invocations == 255 then
Invocations = 0
end
local Invocation = Invocations
Invocations += 1
return Invocation
end
local function Allocate(Bytes: number)
local InUse = (SendCursor + Bytes)
if InUse > SendSize then
--> Avoid resizing the buffer for every write
while InUse > SendSize do
SendSize *= 1.5
end
local Buffer = buffer.create(SendSize)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
SendBuffer = Buffer
end
SendOffset = SendCursor
SendCursor += Bytes
return SendOffset
end
local function CreateQueue(): Queue
return {
head = nil,
tail = nil
}
end
local function Pop(queue: Queue): any
local head = queue.head
if head == nil then
return
end
queue.head = head.next
return head.value
end
local function Push(queue: Queue, value: any)
local entry: Entry = {
value = value,
next = nil
}
if queue.tail ~= nil then
queue.tail.next = entry
end
queue.tail = entry
if queue.head == nil then
queue.head = entry
end
end
local Types = {}
local Calls = table.create(256)
local Events: any = {
Reliable = table.create(256),
Unreliable = table.create(256)
}
local Queue: any = {
Reliable = table.create(256),
Unreliable = table.create(256)
}
return {
}

15
src/server/data.luau Normal file
View file

@ -0,0 +1,15 @@
--!strict
local sapphire_data = require("@pkg/sapphire_data")
export type data = {
coins: number,
}
local template: data = {
coins = 0,
}
return sapphire_data.server({
template = template,
store_name = "player_data",
})

View file

@ -0,0 +1,16 @@
--!strict
local sapphire = require("@pkg/sapphire")
local data = require("@server/data")
local ecs = require("@pkg/sapphire_jecs")
local lifecycles = require("@pkg/sapphire_lifecycles")
local services = script.Parent.services
sapphire()
:register_singletons(services)
:register_singletons(services.systems)
:use(data)
:use(ecs)
:use(lifecycles)
:start()

View file

@ -0,0 +1,22 @@
--!strict
local data = require("@server/data")
local net = require("@net/server")
local function init()
-- this fires every time the data is changed including initial data load
data.on_data_changed(function(player)
local difference = data.calculate_difference(player)
if not difference then
-- weird.. but ok
return
end
net.replicate_data.fire(player, difference)
end)
end
return {
init = init,
-- data is important
priority = math.huge,
}

View file

@ -0,0 +1,31 @@
--!strict
local Players = game:GetService("Players")
local ecs = require("@pkg/sapphire_jecs")
local ref = ecs.ref
local components = require("@shared/components")
local cplayer = components.player
local types = require("@shared/types")
local util = require("@shared/util")
type player = types.RobloxPlayer
type character = types.R15Character
local player_added = ecs.collect(Players.PlayerAdded)
local player_removing = ecs.collect(Players.PlayerRemoving)
local function system()
for _, player: player in player_added do
ref(player.UserId):set(cplayer, player)
end
for _, player: player in player_removing do
ref(player.UserId):delete()
end
end
return {
system = util.wrap_system(system),
phase = "heartbeat",
}

View file

@ -0,0 +1,33 @@
--!strict
local ecs = require("@pkg/sapphire_jecs")
local jecs = ecs.jecs
local world = ecs.world
type entity<T = nil> = ecs.entity<T>
local types = require("@shared/types")
-- change the `Character` type in `types` to whichever character your game is using
type character = types.Character
type player = types.RobloxPlayer<character>
return {
player = world:component() :: entity<player>,
world_model = world:component() :: entity<BasePart>,
-- jecs reexports
on_add = jecs.OnAdd,
on_remove = jecs.OnRemove,
on_set = jecs.OnSet,
child_of = jecs.ChildOf,
componente = jecs.Component,
wildcard = jecs.Wildcard,
w = jecs.Wildcard,
on_delete = jecs.OnDelete,
on_delete_target = jecs.OnDeleteTarget,
delete = jecs.Delete,
remove = jecs.Remove,
name = jecs.Name,
rest = jecs.Rest,
pair = jecs.pair,
}

182
src/shared/types.luau Normal file
View file

@ -0,0 +1,182 @@
--!strict
export type R15CharacterChildren = {
Humanoid: Humanoid & {
Animator: Animator,
},
Shirt: Shirt,
Pants: Pants,
["Body Colors"]: BodyColors,
["Shirt Graphic"]: ShirtGraphic,
HumanoidRootPart: Part & {
Climbing: Sound,
Died: Sound,
FreeFalling: Sound,
GettingUp: Sound,
Jumping: Sound,
Landing: Sound,
Running: Sound,
Splash: Sound,
Swimming: Sound,
RootRigAttachment: Attachment,
},
Head: MeshPart & {
FaceCenterAttachment: Attachment,
FaceFrontAttachment: Attachment,
HairAttachment: Attachment,
HatAttachment: Attachment,
NeckRigAttachment: Attachment,
Neck: Motor6D,
},
LeftFoot: MeshPart & {
LeftAnkleRigAttachment: Attachment,
LeftFootAttachment: Attachment,
LeftFoot: WrapTarget,
LeftAnkle: Motor6D,
},
RightFoot: MeshPart & {
RightAnkleRigAttachment: Attachment,
RightFootAttachment: Attachment,
RightFoot: WrapTarget,
RightAnkle: Motor6D,
},
LeftLowerLeg: MeshPart & {
LeftLowerLeg: WrapTarget,
},
RightLowerLeg: MeshPart & {
RightLowerLeg: WrapTarget,
},
LeftUpperLeg: MeshPart & {
LeftUpperLeg: WrapTarget,
},
RightUpperLeg: MeshPart & {
RightUpperLeg: WrapTarget,
},
LowerTorso: MeshPart & {
LowerTorso: WrapTarget,
},
UpperTorso: MeshPart & {
UpperTorso: WrapTarget,
},
RightHand: MeshPart & {
RightHand: WrapTarget,
},
LeftHand: MeshPart & {
LeftHand: WrapTarget,
},
RightLowerArm: MeshPart & {
RightLowerArm: WrapTarget,
},
RightUpperArm: MeshPart & {
RightUpperArm: WrapTarget,
},
LeftLowerArm: MeshPart & {
LeftElbowRigAttachment: Attachment,
LeftWristAttachment: Attachment,
LeftLowerArm: WrapTarget,
LeftElbow: Motor6D,
},
LeftUpperArm: MeshPart & {
LeftElbowRigAttachment: Attachment,
LeftShoulderAttachment: Attachment,
LeftShoulderRigAttachment: Attachment,
LeftUpperArm: WrapTarget,
LeftShoulder: Motor6D,
},
}
export type R6CharacterChildren = {
Humanoid: Humanoid & {
Animator: Animator,
},
Shirt: Shirt,
Pants: Pants,
["Body Colors"]: BodyColors,
["Shirt Graphic"]: ShirtGraphic,
HumanoidRootPart: Part & {
Climbing: Sound,
Died: Sound,
FreeFalling: Sound,
GettingUp: Sound,
Jumping: Sound,
Landing: Sound,
Running: Sound,
Splash: Sound,
Swimming: Sound,
RootAttachment: Attachment,
RootJoint: Motor6D,
},
Head: Part & {
FaceCenterAttachment: Attachment,
FaceFrontAttachment: Attachment,
HairAttachment: Attachment,
HatAttachment: Attachment,
Mesh: SpecialMesh,
face: Decal,
},
Torso: Part & {
BodyBackAttachment: Attachment,
BodyFrontAttachment: Attachment,
LeftCollarAttachment: Attachment,
NeckAttachment: Attachment,
RightCollarAttachment: Attachment,
WaistBackAttachment: Attachment,
WaistCenterAttachment: Attachment,
WaistFrontAttachment: Attachment,
roblox: Decal,
["Left Hip"]: Motor6D,
["Left Shoulder"]: Motor6D,
Neck: Motor6D,
["Right Hip"]: Motor6D,
["Right Shoulder"]: Motor6D,
},
["Left Arm"]: Part & {
LeftGripAttachment: Attachment,
LeftShoulderAttachment: Attachment,
},
["Left Leg"]: Part & {
LeftFootAttachment: Attachment,
},
["Right Arm"]: Part & {
RightGripAttachment: Attachment,
RightShoulderAttachment: Attachment,
},
["Right Leg"]: Part & {
RightFootAttachment: Attachment,
},
}
export type R15Character = Model & Instance & R15CharacterChildren
export type R6Character = Model & Instance & R6CharacterChildren
-- change it to whatever youre using
export type Character = R15Character
export type RobloxPlayer<Character = Character> = Player & {
PlayerGui: PlayerGui & StarterGui & {
BubbleChat: ScreenGui,
Chat: ScreenGui,
Freecam: ScreenGui,
},
PlayerScripts: PlayerScripts & {
BubbleChat: LocalScript,
ChatScript: LocalScript,
PlayerScriptsLoader: LocalScript,
RbxCharacterSounds: LocalScript,
PlayerModule: ModuleScript,
},
Character: Character?,
CharacterAdded: RBXScriptSignal<Character>,
}
export type Ok<T> = {
kind: "ok",
value: T,
}
export type Err<E> = {
kind: "err",
value: E,
}
export type Result<T, E> = Ok<T> | Err<E>
return {}

28
src/shared/util.luau Normal file
View file

@ -0,0 +1,28 @@
--!strict
--[=[
Useful for wrapping systems for schedulers which take a function returning a system.
(
```lua
-- for example
local function system(world: world)
return function(dt: number)
...
end
end
```
)
@param system
@return () -> (T...) -> ()
]=]
local function wrap_system<T...>(system: (T...) -> ()): () -> (T...) -> ()
return function()
return system
end
end
return {
wrap_system = wrap_system,
}

10
stylua.toml Normal file
View file

@ -0,0 +1,10 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Input"
collapse_simple_statement = "Never"
[sort_requires]
enabled = true