├── .github
└── workflows
│ └── nextjs.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── benchmark
├── .darklua.json
├── Benchmarks.md
├── Performance.luau
├── build.bat
├── build.project.json
├── config.json.example
├── default.project.json
├── definitions
│ ├── Definition.blink
│ └── Definition.zap
├── download.luau
├── generate.luau
├── run.luau
└── src
│ ├── client
│ └── init.client.luau
│ ├── server
│ └── init.server.luau
│ └── shared
│ ├── benches
│ ├── Booleans.luau
│ ├── Entities.luau
│ └── init.luau
│ └── modes
│ ├── blink.luau
│ ├── bytenet.luau
│ ├── init.luau
│ ├── roblox.luau
│ ├── warp.luau
│ └── zap.luau
├── build
├── .darklua.json
├── build.bat
├── compile.luau
├── init.luau
└── version.luau
├── default.project.json
├── docs
├── .gitignore
├── README.md
├── components
│ ├── counters.module.css
│ └── counters.tsx
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── pages
│ ├── _meta.json
│ ├── getting-started
│ │ ├── 1-installation.mdx
│ │ ├── 2-introduction.mdx
│ │ ├── 3-cli.mdx
│ │ ├── 4-plugin.mdx
│ │ └── _meta.json
│ ├── index.mdx
│ └── language
│ │ ├── 1-options.mdx
│ │ ├── 2-scopes.mdx
│ │ ├── 3-imports.mdx
│ │ ├── 4-types.mdx
│ │ ├── 5-events.mdx
│ │ ├── 6-functions.mdx
│ │ └── _meta.json
├── pnpm-lock.yaml
├── public
│ ├── Logo.png
│ ├── letter.png
│ ├── plugin
│ │ ├── Duplicate.png
│ │ ├── Editor.png
│ │ ├── Error.png
│ │ ├── Generate.png
│ │ ├── GenerateSelected.png
│ │ ├── Invalid.png
│ │ ├── Locate.png
│ │ ├── Menu.png
│ │ ├── Navigation.png
│ │ ├── Output.png
│ │ └── Save.png
│ └── syntax
│ │ ├── LICENSE
│ │ ├── blink.tmLanguage.json
│ │ └── mocha.json
├── theme.config.tsx
└── tsconfig.json
├── plugin
├── .darklua.json
├── build.bat
├── bundle.project.json
└── src
│ ├── Editor
│ ├── Styling
│ │ ├── AutoBracket.luau
│ │ └── AutoIndent.luau
│ ├── Utility.luau
│ └── init.luau
│ ├── Error.rbxmx
│ ├── Profiler.luau
│ ├── State.luau
│ ├── Table.luau
│ ├── Widget.rbxmx
│ └── init.server.luau
├── rokit.toml
├── src
├── CLI
│ ├── Utility
│ │ ├── Compile.luau
│ │ ├── GetDefinitionFilePath.luau
│ │ └── Watch.luau
│ └── init.luau
├── Generator
│ ├── Blocks.luau
│ ├── Prefabs.luau
│ ├── Typescript.luau
│ ├── Util.luau
│ └── init.luau
├── Lexer.luau
├── Modules
│ ├── Builder.luau
│ ├── Error.luau
│ ├── Format.luau
│ ├── Path.luau
│ └── Table.luau
├── Parser.luau
├── Settings.luau
└── Templates
│ ├── Base.luau
│ ├── Client.luau
│ └── Server.luau
└── test
├── Client.luau
├── Server.luau
├── Shared.luau
├── Sources
├── Generics.blink
├── Import.blink
├── Indexers.blink
├── NoGenerics.blink
├── Polling.blink
├── Scope.blink
├── Sub-Sources
│ ├── Source.blink
│ └── Source2.blink
└── Test.blink
└── Test.luau
/.github/workflows/nextjs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GitHub Pages
2 |
3 | on:
4 | release:
5 | types: [released]
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | id-token: write
12 |
13 | concurrency:
14 | group: "gh-pages"
15 | cancel-in-progress: false
16 |
17 | jobs:
18 | build:
19 | name: "Build"
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v4
24 |
25 | - name: Setup Node
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: "18.17.0"
29 |
30 | - name: Setup Pages
31 | uses: actions/configure-pages@v4
32 | with:
33 | static_site_generator: next
34 |
35 | - name: Install dependencies
36 | working-directory: ./docs
37 | run: npm install
38 |
39 | - name: Build
40 | working-directory: ./docs
41 | run: npm run build
42 |
43 | - name: Upload artifact
44 | uses: actions/upload-pages-artifact@v3
45 | with:
46 | path: ./docs/out
47 |
48 | deploy:
49 | name: "Deploy"
50 | environment:
51 | name: github-pages
52 | url: ${{ steps.deployment.outputs.page_url }}
53 | runs-on: ubuntu-latest
54 | needs: build
55 | steps:
56 | - name: Deploy to GitHub Pages
57 | id: deployment
58 | uses: actions/deploy-pages@v4
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project place file
2 | /src.rbxlx
3 |
4 | # Roblox Studio lock files
5 | /*.rbxlx.lock
6 | /*.rbxl.lock
7 |
8 | /release
9 | /Network
10 | /Blink
11 | test/Sources/Game.txt
12 | sourcemap.json
13 | /plugin/bundle
14 |
15 | /benchmark/tools
16 | /benchmark/packages
17 | /benchmark/config.json
18 | /benchmark/src/shared/blink
19 | /benchmark/src/shared/zap
20 | /benchmark/roblox
21 |
22 | /site
23 |
24 | *.rbxl
25 | *.rbxl.lock
26 | test/Sources/Issue.blink
27 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "luau-lsp.plugin.enabled": true,
3 | "luau-lsp.require.mode": "relativeToFile",
4 | "luau-lsp.require.directoryAliases": {
5 | "@lune/": "~/.lune/.typedefs/0.8.9/"
6 | },
7 | "luau-lsp.sourcemap.rojoProjectFile": "default.project.json"
8 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Axen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | [](LICENSE)
6 | [](https://github.com/1Axen/blink/releases/latest)
7 |
8 | An IDL compiler written in Luau for ROBLOX buffer networking
9 |
10 | # Performance
11 | Blink aims to generate the most performant and bandwidth-efficient code for your specific experience, but what does this mean?
12 |
13 | It means lower bandwidth usage directly resulting in **lower ping\*** experienced by players and secondly, it means **lower CPU usage** compared to more generalized networking solutions.
14 |
15 | *\* In comparison to standard ROBLOX networking, this may not always be the case but should never result in increased ping times.*
16 |
17 | Benchmarks are available here [here](./benchmark/Benchmarks.md).
18 |
19 | # Security
20 | Blink does two things to combat bad actors:
21 | 1. Data sent by clients will be **validated** on the receiving side before reaching any critical game code.
22 | 2. As a result of the compression done by Blink it becomes **significantly harder** to snoop on your game's network traffic. Long gone are the days of skids using RemoteSpy to snoop on your game's traffic.
23 |
24 | # Get Started
25 | Head over to the [installation](https://1axen.github.io/blink/getting-started/1-installation) page to get started with Blink.
26 |
27 | # Credits
28 | Credits to [Zap](https://zap.redblox.dev/) for the range and array syntax
29 | Credits to [ArvidSilverlock](https://github.com/ArvidSilverlock) for the float16 implementation
30 | Studio plugin auto completion icons are sourced from [Microsoft](https://github.com/microsoft/vscode-icons) and are under the [CC BY 4.0](https://github.com/microsoft/vscode-icons/blob/main/LICENSE) license.
31 | Speed icons created by alkhalifi design - Flaticon
32 |
--------------------------------------------------------------------------------
/benchmark/.darklua.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator": "retain_lines",
3 | "rules": [
4 | {
5 | "rule": "convert_require",
6 | "current": {
7 | "name": "path"
8 | },
9 | "target": {
10 | "name": "roblox",
11 | "rojo_sourcemap": "./sourcemap.json",
12 | "indexing_style": "property"
13 | }
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/benchmark/Benchmarks.md:
--------------------------------------------------------------------------------
1 | # Benchmarks
2 | ## Methodology
3 | Benchmarks are done by firing the event 1000 times per frame with the same data every frame for 10 seconds.
4 |
5 | Source code can be found here [here](https://github.com/1Axen/Blink/blob/main/benchmark/src).
6 | Data used for benchmarks can be found [here](https://github.com/1Axen/Blink/blob/main/benchmark/src/shared/benches).
7 | Defenition files used for benchmarks can be found [here](https://github.com/1Axen/Blink/blob/main/benchmark/definitions).
8 |
9 | ## Results
10 |
11 | `P[NUMBER]` = [NUMBER] Percentile
12 | *The tables below were automatically generated by this [script](https://github.com/1Axen/Blink/blob/main/benchmark/generate.luau).*
13 | ## Last Updated 2025-04-30 19:45:09 UTC
14 | ## Tool Versions
15 | `blink`: v0.17.1
16 | `zap`: v0.6.20
17 | `bytenet`: v0.4.3
18 | ## Computer Specs
19 | Processor: `AMD Ryzen 9 7900X 12-Core Processor `
20 | Memory #1: `17GB 4800`
21 | Memory #2: `17GB 4800`
22 | ## [Entities](https://github.com/1Axen/Blink/blob/main/benchmark/src/shared/benches/Entities.luau)
23 | |Tool (FPS)|Median|P0|P80|P90|P95|P100|Loss (%)|
24 | |---|---|---|---|---|---|---|---|
25 | |roblox|16.00|16.00|15.00|15.00|15.00|15.00|0%|
26 | |blink|42.00|45.00|42.00|42.00|42.00|42.00|0%|
27 | |zap|39.00|40.00|38.00|38.00|38.00|38.00|0%|
28 | |bytenet|32.00|34.00|32.00|32.00|32.00|31.00|0%|
29 |
30 | |Tool (Kbps)|Median|P0|P80|P90|P95|P100|Loss (%)|
31 | |---|---|---|---|---|---|---|---|
32 | |roblox|559364.31|559364.31|676715.68|676715.68|676715.68|784081.75|0%|
33 | |blink|41.81|26.30|42.40|42.48|42.48|42.62|0%|
34 | |zap|41.71|25.46|42.19|42.32|42.32|42.93|0%|
35 | |bytenet|41.64|22.84|42.36|42.82|42.82|43.24|0%|
36 | ## [Booleans](https://github.com/1Axen/Blink/blob/main/benchmark/src/shared/benches/Booleans.luau)
37 | |Tool (FPS)|Median|P0|P80|P90|P95|P100|Loss (%)|
38 | |---|---|---|---|---|---|---|---|
39 | |roblox|21.00|22.00|20.00|19.00|19.00|19.00|0%|
40 | |blink|97.00|98.00|97.00|96.00|96.00|96.00|0%|
41 | |zap|52.00|53.00|51.00|51.00|51.00|49.00|0%|
42 | |bytenet|35.00|37.00|35.00|35.00|35.00|34.00|0%|
43 |
44 | |Tool (Kbps)|Median|P0|P80|P90|P95|P100|Loss (%)|
45 | |---|---|---|---|---|---|---|---|
46 | |roblox|353107.13|196826.86|690747.68|842240.25|842240.25|1124176.38|0%|
47 | |blink|7.91|7.41|7.93|7.99|7.99|8.00|0%|
48 | |zap|8.10|5.75|8.17|8.22|8.22|8.27|0%|
49 | |bytenet|8.11|5.07|8.35|8.46|8.46|8.47|0%|
--------------------------------------------------------------------------------
/benchmark/Performance.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 | local stdio = require("@lune/stdio")
3 |
4 | local Lexer = require("../src/Lexer")
5 | local Parser = require("../src/Parser")
6 | local Generator = require("../src/Generator")
7 |
8 | local Source = fs.readFile("../test/Sources/Test.txt")
9 | local SourceLexer = Lexer.new()
10 | local SourceParser = Parser.new("../test/Sources/")
11 | local AbstractSyntaxTree = SourceParser:Parse(Source)
12 |
13 | local function Format(Time: number): string
14 | local Suffix = "seconds"
15 | if Time < 1E-6 then
16 | Time *= 1E+9
17 | Suffix = "ns"
18 | elseif Time < 0.001 then
19 | Time *= 1E+6
20 | Suffix = "μs"
21 | elseif Time < 1 then
22 | Time *= 1000
23 | Suffix = "ms"
24 | end
25 |
26 | return `{stdio.color("green")}{string.format("%.2f", Time)} {Suffix}{stdio.color("reset")}`
27 | end
28 |
29 | local function Percentile(Samples: {number}, Percent: number): number
30 | local Index = (#Samples * Percent) // 1
31 | Index = math.max(Index, 1)
32 | return Samples[Index]
33 | end
34 |
35 | local function BenchmarkClosure(Title: string, Closure: () -> ())
36 | print(`Benchmarking: {Title}`)
37 | stdio.write("Running")
38 |
39 | local Times = {}
40 | for Index = 1, 1_000 do
41 | stdio.write(`\rRunning [{stdio.color("green")}{Index}{stdio.color("reset")}/1000]`)
42 |
43 | local Start = os.clock()
44 | Closure()
45 |
46 | local Elapsed = (os.clock() - Start)
47 | table.insert(Times, Elapsed)
48 | end
49 |
50 | table.sort(Times, function(a, b)
51 | return a < b
52 | end)
53 |
54 | stdio.write(`\r-------- {stdio.color("yellow")}{Title}{stdio.color("reset")} --------\n`)
55 | print(`Median: {Format(Percentile(Times, 0.5))}`)
56 | print(`0th Percentile: {Format(Times[1])}`)
57 | print(`90th Percentile: {Format(Percentile(Times, 0.9))}`)
58 | print(`95th Percentile: {Format(Percentile(Times, 0.95))}`)
59 | end
60 |
61 | _G.BUNDLED = true
62 | _G.VERSION = "PERFROMANCE"
63 |
64 | BenchmarkClosure("Lex", function()
65 | SourceLexer:Initialize(Source)
66 |
67 | while true do
68 | local Token = SourceLexer:GetNextToken()
69 | if Token.Type == "EndOfFile" then
70 | break
71 | end
72 | end
73 | end)
74 |
75 | BenchmarkClosure("Parse", function()
76 | SourceParser:Parse(Source)
77 | end)
78 |
79 | BenchmarkClosure("Generate", function()
80 | Generator.Generate("Server", AbstractSyntaxTree)
81 | end)
82 |
--------------------------------------------------------------------------------
/benchmark/build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | setlocal
4 | :PROMPT
5 | SET "AREYOUSURE=N"
6 | SET /P "AREYOUSURE=Download tools? (Y/[N])?"
7 | IF /I %AREYOUSURE% NEQ Y GOTO COMPILE
8 |
9 | echo Downloading dependencies...
10 | rmdir /s /q ".\tools"
11 | rmdir /s /q ".\packages"
12 | mkdir ".\tools"
13 | mkdir ".\packages"
14 | lune run download
15 | del /q ".\tools\*.zip"
16 | del /q ".\packages\*.zip"
17 |
18 | :COMPILE
19 | echo Compiling definitions
20 | if not exist "./src/shared/zap" mkdir "./src/shared/zap"
21 | if not exist "./src/shared/blink" mkdir "./src/shared/blink"
22 | "./tools/zap.exe" "./definitions/Definition.zap"
23 | "./tools/blink.exe" "./definitions/Definition.blink"
24 | endlocal
25 |
26 | echo Building ROBLOX place
27 | if not exist roblox mkdir roblox
28 | rojo sourcemap default.project.json --output sourcemap.json --include-non-scripts
29 | REM Update sourcemap used for luau-lsp
30 | rojo sourcemap default.project.json --output ../sourcemap.json --include-non-scripts
31 | darklua process src roblox
32 | rojo build build.project.json --output "./Benchmark.rbxl"
33 |
34 | setlocal
35 | SET "OPENSTUDIO=N"
36 | SET /P OPENSTUDIO=Open generated place? (Y/[N])?
37 | if /I %OPENSTUDIO% NEQ Y GOTO END
38 |
39 | lune run generate
40 |
41 | :END
42 | endlocal
--------------------------------------------------------------------------------
/benchmark/build.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "benchmark",
3 | "emitLegacyScripts": false,
4 | "tree": {
5 | "$className": "DataModel",
6 |
7 | "Players": {
8 | "$properties": {
9 | "CharacterAutoLoads": false
10 | }
11 | },
12 |
13 | "ReplicatedStorage": {
14 | "Shared": {
15 | "$path": "roblox/shared",
16 | "Packages": {
17 | "$path": "packages"
18 | },
19 | "GetRecieved": {
20 | "$className": "RemoteFunction"
21 | },
22 | "Generate": {
23 | "$className": "RemoteEvent"
24 | }
25 | },
26 | "Client": {
27 | "$path": "roblox/client"
28 | }
29 | },
30 |
31 | "ServerScriptService": {
32 | "Server": {
33 | "$path": "roblox/server"
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/benchmark/config.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "github-token": ""
3 | }
--------------------------------------------------------------------------------
/benchmark/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "benchmark",
3 | "emitLegacyScripts": false,
4 | "tree": {
5 | "$className": "DataModel",
6 |
7 | "ReplicatedStorage": {
8 | "Shared": {
9 | "$path": "src/shared",
10 | "Packages": {
11 | "$path": "packages"
12 | },
13 | "GetRecieved": {
14 | "$className": "RemoteFunction"
15 | },
16 | "Generate": {
17 | "$className": "RemoteEvent"
18 | }
19 | },
20 | "Client": {
21 | "$path": "src/client"
22 | }
23 | },
24 |
25 | "ServerScriptService": {
26 | "Server": {
27 | "$path": "src/server"
28 | }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/benchmark/definitions/Definition.blink:
--------------------------------------------------------------------------------
1 | option ClientOutput = "../src/shared/blink/Client.luau"
2 | option ServerOutput = "../src/shared/blink/Server.luau"
3 |
4 | struct Entity {
5 | id: u8,
6 | x: u8,
7 | y: u8,
8 | z: u8,
9 | orientation: u8,
10 | animation: u8
11 | }
12 |
13 | event Booleans {
14 | From: Client,
15 | Type: Reliable,
16 | Call: SingleSync,
17 | Data: boolean[0..1000]
18 | }
19 |
20 | event Entities {
21 | From: Client,
22 | Type: Reliable,
23 | Call: SingleSync,
24 | Data: Entity[0..1000]
25 | }
--------------------------------------------------------------------------------
/benchmark/definitions/Definition.zap:
--------------------------------------------------------------------------------
1 | opt client_output = "../src/shared/zap/Client.luau"
2 | opt server_output = "../src/shared/zap/Server.luau"
3 |
4 | type Entity = struct {
5 | id: u8,
6 | x: u8,
7 | y: u8,
8 | z: u8,
9 | orientation: u8,
10 | animation: u8
11 | }
12 |
13 | event Booleans = {
14 | from: Client,
15 | type: Reliable,
16 | call: SingleSync,
17 | data: boolean[0..1000]
18 | }
19 |
20 | event Entities = {
21 | from: Client,
22 | type: Reliable,
23 | call: SingleSync,
24 | data: Entity[0..1000]
25 | }
--------------------------------------------------------------------------------
/benchmark/download.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 | local net = require("@lune/net")
3 | local process = require("@lune/process")
4 |
5 | local AUTH_TOKEN;
6 | local PATH_TO_7ZIP = "C:\\Program Files\\7-Zip\\7z.exe"
7 | local PATH_TO_TOOLS = "./tools/"
8 | local PATH_TO_PACKAGES = "./packages/"
9 |
10 | local Tools = {
11 | zap = "https://api.github.com/repos/red-blox/zap/releases/latest",
12 | blink = "https://api.github.com/repos/1Axen/blink/releases/latest",
13 | }
14 |
15 | local Packages: {[string]: {GitHub: string, Wally: string}} = {
16 | -- FIXME: Doesn't download correctly
17 | --[[warp = {
18 | GitHub = "https://api.github.com/repos/imezx/Warp/releases",
19 | Wally = "https://api.wally.run/v1/package-contents/imezx/warp/"
20 | },]]
21 |
22 | bytenet = {
23 | GitHub = "https://api.github.com/repos/ffrostflame/ByteNet/releases",
24 | Wally = "https://api.wally.run/v1/package-contents/ffrostflame/bytenet/"
25 | }
26 | }
27 |
28 | local Versions = {}
29 |
30 | if fs.isFile("config.json") then
31 | local Configuration = net.jsonDecode(fs.readFile("config.json"))
32 | AUTH_TOKEN = Configuration["github-token"]
33 | print(`Using github token to do API requests!`)
34 | end
35 |
36 | local function Curl(URL: string, Output: string, Headers: {string}?): process.SpawnResult
37 | local Arguments = {}
38 |
39 | if AUTH_TOKEN then
40 | table.insert(Arguments, "--header")
41 | table.insert(Arguments, `Authorization: Bearer {AUTH_TOKEN}`)
42 | end
43 |
44 | if Headers then
45 | for Index, Header in Headers do
46 | table.insert(Arguments, "--header")
47 | table.insert(Arguments, Header)
48 | end
49 | end
50 |
51 | table.insert(Arguments, "--location")
52 | table.insert(Arguments, URL)
53 |
54 | table.insert(Arguments, "--output")
55 | table.insert(Arguments, Output)
56 |
57 | return process.spawn("curl", Arguments)
58 | end
59 |
60 | local function Request(Fetch: net.FetchParams): net.FetchResponse
61 | if AUTH_TOKEN then
62 | Fetch.headers = Fetch.headers or {}
63 | (Fetch.headers :: any).Authorization = `Bearer {AUTH_TOKEN}`
64 | end
65 |
66 | local Response = net.request(Fetch)
67 | local Ratelimit = Response.headers["x-ratelimit-remaining"]
68 |
69 | if tonumber(Ratelimit) == 0 then
70 | error(`API rate limit exceeded, retry again in {Response.headers["x-ratelimit-reset"] - os.time()} seconds.`)
71 | end
72 |
73 | assert(Response.ok, Response.statusMessage)
74 | return Response
75 | end
76 |
77 | for Name, Url in Tools do
78 | local Response = Request({
79 | url = Url,
80 | method = "GET"
81 | })
82 |
83 | local Release = net.jsonDecode(Response.body)
84 | local DownloadUrl: string
85 |
86 | for Index, Asset in Release.assets do
87 | if not string.find(Asset.name, "windows-x86_64", 1, true) then
88 | continue
89 | end
90 |
91 | DownloadUrl = Asset.browser_download_url
92 | end
93 |
94 | assert(DownloadUrl, `Unable to find download URL for {Name}`)
95 |
96 | local CurlResult = Curl(DownloadUrl, `{PATH_TO_TOOLS}{Name}.zip`)
97 | assert(CurlResult.ok, `Encountered an exception while downloading files for {Name}, {CurlResult.code}`)
98 |
99 | local UnzipResult = process.spawn(`{PATH_TO_7ZIP}`, {"e", `{PATH_TO_TOOLS}{Name}.zip`, "-y", `-o{PATH_TO_TOOLS}`})
100 | assert(UnzipResult.ok, `Encountered an exception while unzipping downloads for {Name}, {UnzipResult.stderr}`)
101 |
102 | Versions[Name] = Release.tag_name
103 | print("Downloaded", Name, Release.tag_name)
104 | end
105 |
106 | for Name, Urls in Packages do
107 | local Response = Request({
108 | url = Urls.GitHub,
109 | method = "GET"
110 | })
111 |
112 | local Releases = net.jsonDecode(Response.body)
113 | table.sort(Releases, function(a, b)
114 | return a.id > b.id
115 | end)
116 |
117 | local Release = Releases[1]
118 | local Version = Release.tag_name
119 | local PackageVersion = Version
120 |
121 | local ZipPath = `{PATH_TO_PACKAGES}{Name}_ZIP`
122 | local FinalPath = `{PATH_TO_PACKAGES}{Name}`
123 |
124 | if string.sub(Version, 1, 1) == "v" then
125 | PackageVersion = string.sub(Version, 2)
126 | end
127 |
128 | local DownloadUrl = `{Urls.Wally}{PackageVersion}`
129 | local CurlResult = Curl(DownloadUrl, `{ZipPath}.zip`, {"Wally-Version: 0.3.2"})
130 | assert(CurlResult.ok, `Encountered an exception while downloading files for {Name}, {CurlResult.stderr}`)
131 |
132 | if not fs.isDir(ZipPath) then
133 | fs.writeDir(ZipPath)
134 | end
135 |
136 | local UnzipResult = process.spawn(`{PATH_TO_7ZIP}`, {"x", `{ZipPath}.zip`, "-y", `-o{ZipPath}`})
137 | assert(UnzipResult.ok, `Encountered an exception while unzipping downloads for {Name}, {UnzipResult.stderr}`)
138 |
139 | fs.move(`{ZipPath}/src`, FinalPath)
140 | fs.removeDir(ZipPath)
141 |
142 | Versions[Name] = Version
143 | print("Downloaded", Name, Version)
144 | end
145 |
146 | fs.writeFile("./tools/versions.json", net.jsonEncode(Versions))
--------------------------------------------------------------------------------
/benchmark/generate.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 | local net = require("@lune/net")
3 | local process = require("@lune/process")
4 | local DateTime = require("@lune/datetime")
5 |
6 | local VersionsJSON = fs.readFile("./tools/versions.json")
7 | local Versions = net.jsonDecode(VersionsJSON)
8 |
9 | local RobloxResult = process.spawn("run-in-roblox", {"--place", "Benchmark.rbxl", "--script", "run.luau"})
10 | if not RobloxResult.ok then
11 | error(RobloxResult.stderr)
12 | end
13 |
14 | local Stdout = string.split(RobloxResult.stdout, "--RESULTS JSON--")
15 | table.remove(Stdout, 1)
16 |
17 | local Result: {[string]: {[string]: Benchmark}};
18 | for _, Line in Stdout do
19 | Line = string.gsub(Line, "\n", "")
20 | if string.sub(Line, 1, 1) == "{" then
21 | Result = net.jsonDecode(Line)
22 | break
23 | end
24 | end
25 |
26 | local Seperator = "*The tables below were automatically generated by this [script](https://github.com/1Axen/Blink/blob/main/benchmark/generate.luau).*"
27 | local Header = string.split(fs.readFile("./Benchmarks.md"), Seperator)
28 | local Contents = Header[1] .. Seperator
29 |
30 | Contents ..= `\n## Last Updated {DateTime.now():formatUniversalTime()} UTC`
31 | Contents ..= `\n## Tool Versions`
32 |
33 | for Tool, Version in Versions do
34 | Contents ..= `\n\`{Tool}\`: {Version} `
35 | end
36 |
37 | local function QueryWMIC(Component: string, Properties: string): {string}
38 | local SpawnResult = process.spawn("wmic", {Component, "get", Properties})
39 | assert(SpawnResult.ok, SpawnResult.stderr)
40 |
41 | local Lines = string.split(SpawnResult.stdout, "\n")
42 |
43 | --> Remove header and padding
44 | table.remove(Lines, 1)
45 | table.remove(Lines, #Lines)
46 | table.remove(Lines, #Lines)
47 |
48 | --> Parse text
49 | local QueryResult = {}
50 | for Index, Line in Lines do
51 | --> Remove padding
52 | Line = string.gsub(Line, "\r", "")
53 | Line = string.sub(Line, 1, #Line - 1)
54 |
55 | if Component == "cpu" then
56 | table.insert(QueryResult, Line)
57 | continue
58 | end
59 |
60 | local SubResult = {}
61 | local LineResults = string.split(Line, " ")
62 | for Index, Result in LineResults do
63 | --> Ignore empty lines
64 | if string.gsub(Result, "%c", "") == "" then
65 | continue
66 | end
67 |
68 | table.insert(SubResult, Result)
69 | end
70 |
71 | table.insert(QueryResult, SubResult)
72 | end
73 |
74 | return QueryResult
75 | end
76 |
77 | local function WriteComputerSpecs()
78 | local Processor = QueryWMIC("cpu", "name")
79 | local MemorySticks = QueryWMIC("memorychip", "Capacity,Speed")
80 |
81 | Contents ..= `\n## Computer Specs`
82 | Contents ..= `\nProcessor: \`{Processor[1]}\` `
83 |
84 | for Slot, Stick in MemorySticks do
85 | local Capacity = tonumber(Stick[1])
86 | Capacity //= 1e+9
87 | Contents ..= `\nMemory #{Slot}: \`{Capacity}GB {Stick[2]}\` `
88 | end
89 | end
90 |
91 | type Benchmark = {
92 | Sent: number,
93 | Recieve: number,
94 | Bandwidth: {number},
95 | Framerate: {number}
96 | }
97 |
98 | local Metrics = {
99 | {
100 | Label = "FPS",
101 | Samples = "Framerate"
102 | },
103 | {
104 | Label = "Kbps",
105 | Samples = "Bandwidth"
106 | }
107 | }
108 |
109 | local function WriteBenchResults(Name: string, Results: {[string]: Benchmark})
110 | local Output = `\n## [{Name}](https://github.com/1Axen/Blink/blob/main/benchmark/src/shared/benches/{Name}.luau)\n`
111 |
112 | for Index, Metric in Metrics do
113 | if Index > 1 then
114 | Output ..= "\n\n"
115 | end
116 |
117 | Output ..= `|Tool ({Metric.Label})|Median|P0|P80|P90|P95|P100|Loss (%)|`
118 | Output ..= `\n|---|---|---|---|---|---|---|---|`
119 | for Tool, Result in Results do
120 | local Loss = math.floor((1 - (Result.Recieve / Result.Sent)) * 100)
121 | if Loss > 50 then
122 | Output ..= `\n|{Tool}|-|-|-|-|-|-|{Loss}%`
123 | continue
124 | end
125 |
126 | local Numbers = Result[Metric.Samples]
127 | local Formatted = table.create(#Numbers)
128 | for Index, Value in Numbers do
129 | Formatted[Index] = string.format("%.2f", Value)
130 | end
131 |
132 | Output ..= `\n|{Tool}|{table.concat(Formatted, "|")}|{Loss}%|`
133 | end
134 | end
135 |
136 | Contents ..= Output
137 | end
138 |
139 | --> Write computer specs
140 | WriteComputerSpecs()
141 |
142 | --> Sort tools alphabetically
143 | local Array = {}
144 | for Name, Benchmarks in Result do
145 | table.insert(Array, Name)
146 | end
147 |
148 | table.sort(Array, function(a, b)
149 | return #a < #b
150 | end)
151 |
152 | for _, Name in Array do
153 | WriteBenchResults(Name, Result[Name])
154 | end
155 |
156 | --> Output results
157 | fs.writeFile("./Benchmarks.md", Contents)
158 | print(`Success!`)
--------------------------------------------------------------------------------
/benchmark/run.luau:
--------------------------------------------------------------------------------
1 | local RunService = game:GetService("RunService")
2 | if not RunService:IsRunning() then
3 | print("Press F5 to start the benchmark.")
4 | while task.wait(9e9) do end
5 | end
6 |
7 | local Result: StringValue;
8 | while Result == nil do
9 | Result = game:FindFirstChild("Result")
10 | task.wait(1)
11 | end
12 |
13 | print("--RESULTS JSON--")
14 | print(Result.Value)
15 |
--------------------------------------------------------------------------------
/benchmark/src/client/init.client.luau:
--------------------------------------------------------------------------------
1 | local Modes = require("../shared/modes")
2 | local Benches = require("../shared/benches")
3 |
4 | local Players = game:GetService("Players")
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local HttpService = game:GetService("HttpService")
7 | local RunService = game:GetService("RunService")
8 | local StarterGui = game:GetService("StarterGui")
9 | local Stats = game:GetService("Stats")
10 |
11 | local MAXIMUM_FRAMERATE = 60
12 |
13 | local Camera = workspace.CurrentCamera
14 | Camera.FieldOfView = 1
15 | Camera.CFrame = CFrame.new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
16 |
17 | Players.LocalPlayer.PlayerGui:ClearAllChildren()
18 | Players.LocalPlayer.PlayerScripts:ClearAllChildren()
19 |
20 | for _, Item in Enum.CoreGuiType:GetEnumItems() do
21 | while true do
22 | local Success = pcall(function()
23 | StarterGui:SetCoreGuiEnabled(Item, false)
24 | end)
25 |
26 | if Success then
27 | break
28 | end
29 | end
30 | end
31 |
32 | type Benchmark = {
33 | Sent: number,
34 | Recieve: number,
35 | Bandwidth: {number},
36 | Framerate: {number}
37 | }
38 |
39 | type Results = {
40 | [string]: {
41 | [string]: Benchmark
42 | }
43 | }
44 |
45 | local function Percentile(Samples: {number}, Percentile: number)
46 | assert((Percentile // 1) == Percentile and Percentile >= 0 and Percentile <= 100, "Percentile must be an integer between 0 and 100")
47 | local Index = ((#Samples * (Percentile / 100)) // 1)
48 | Index = math.max(Index, 1)
49 | return Samples[Index]
50 | end
51 |
52 | local function WaitForPacketsToProcess()
53 | print("Waiting for packets to be processed.")
54 | while (Stats.DataSendKbps > 0.5) do
55 | RunService.Heartbeat:Wait()
56 | end
57 |
58 | task.wait(5)
59 | end
60 |
61 | local function RunBenchmark(Tool: string, Bench: string): Benchmark
62 | local Total = 0
63 | local Frames = 0
64 | local Bandwidth = {}
65 | local Framerates = {}
66 |
67 | local Sent = 0
68 | local Data = Benches[Bench]
69 | local Events = Modes[Tool]
70 |
71 | local Event = Events[Bench]
72 | local Method;
73 |
74 | if Tool == "warp" then
75 | Method = function(Data)
76 | Event:Fire(true, Data)
77 | end
78 | elseif Tool == "roblox" then
79 | Method = function(Data)
80 | Event:FireServer(Data)
81 | end
82 | else
83 | Method = Event.Fire or Event.send
84 | end
85 |
86 | local Connection = RunService.PostSimulation:Connect(function(DeltaTime: number)
87 | Total += DeltaTime
88 | Frames += 1
89 |
90 | if Total >= 1 then
91 | Total -= 1
92 | local Scale = (MAXIMUM_FRAMERATE / Frames)
93 | table.insert(Bandwidth, Stats.DataSendKbps * Scale)
94 | table.insert(Framerates, Frames)
95 | Frames = 0
96 | end
97 |
98 | for Index = 1, 1000 do
99 | Sent += 1
100 | Method(Data)
101 | end
102 | end)
103 |
104 | task.wait(10)
105 | Connection:Disconnect()
106 | print(`> Finished running with {Tool}`)
107 |
108 | --> Generate results
109 | table.sort(Bandwidth)
110 | table.sort(Framerates, function(a, b)
111 | return a > b
112 | end)
113 |
114 | local FrameratePercentiles = {}
115 | local BandwidthPercentiles = {}
116 |
117 | for _, Percentage in {50, 0, 80, 90, 95, 100} do
118 | table.insert(BandwidthPercentiles, Percentile(Bandwidth, Percentage))
119 | table.insert(FrameratePercentiles, Percentile(Framerates, Percentage))
120 | end
121 |
122 | return {
123 | Sent = Sent,
124 | Recieve = 0,
125 | Bandwidth = BandwidthPercentiles,
126 | Framerate = FrameratePercentiles
127 | }
128 | end
129 |
130 | local function RunBenchmarks()
131 | local Results: Results = {}
132 | for Bench in Benches do
133 | warn(`Running {Bench} benchmark`)
134 | Results[Bench] = {}
135 |
136 | for Tool in Modes do
137 | print(`> Running with {Tool}`)
138 | Results[Bench][Tool] = RunBenchmark(Tool, Bench)
139 |
140 | --> Give ROBLOX some time to rest in-between tools
141 | WaitForPacketsToProcess()
142 | end
143 |
144 | --> Give ROBLOX some time to rest in-between benchmarks
145 | WaitForPacketsToProcess()
146 | end
147 |
148 | local Recieved = ReplicatedStorage.Shared.GetRecieved:InvokeServer()
149 | for Bench, Tools in Recieved do
150 | for Tool, Recieve in Tools do
151 | Results[Bench][Tool].Recieve = Recieve
152 | end
153 | end
154 |
155 | print("Finished running benchmarks, generating results...")
156 | ReplicatedStorage.Shared.Generate:FireServer(HttpService:JSONEncode(Results))
157 | end
158 |
159 | RunBenchmarks()
--------------------------------------------------------------------------------
/benchmark/src/server/init.server.luau:
--------------------------------------------------------------------------------
1 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
2 | local HttpService = game:GetService("HttpService")
3 |
4 | local Modes = require("../shared/modes")
5 | local Benches = require("../shared/benches")
6 |
7 | function CompareTables(a, b)
8 | if a == b then
9 | return true
10 | end
11 |
12 | if type(a) ~= type(b) then
13 | return false
14 | end
15 |
16 | if type(a) ~= "table" then
17 | return false
18 | end
19 |
20 | local Keys = {}
21 | for Key, Value in a do
22 | local SecondaryValue = b[Key]
23 | if SecondaryValue == nil or not CompareTables(Value, SecondaryValue) then
24 | return false
25 | end
26 | Keys[Key] = true
27 | end
28 |
29 | for Key, _ in b do
30 | if not Keys[Key] then
31 | return false
32 | end
33 | end
34 |
35 | return true
36 | end
37 |
38 | local function CompareValues(a, b): boolean
39 | if type(a) == "table" or type(b) == "table" then
40 | return CompareTables(a, b)
41 | end
42 | return (a == b)
43 | end
44 |
45 | local Recieved = {}
46 | for Name, Data in Benches do
47 | local BenchRecieved = {}
48 | Recieved[Name] = BenchRecieved
49 |
50 | local function OnRecieve(Tool: string)
51 | BenchRecieved[Tool] = 0
52 |
53 | return function(Player, Recieve)
54 | BenchRecieved[Tool] += 1
55 | if BenchRecieved[Tool] > 1 then
56 | return
57 | end
58 |
59 | if Tool == "bytenet" then
60 | Player, Recieve = Recieve, Player
61 | end
62 |
63 | if not CompareValues(Data, Recieve) then
64 | warn(`Recieved incorrect data with {Tool} for {Name}`)
65 | else
66 | print(`> {Tool} passed {Name} validation!`)
67 | end
68 | end
69 | end
70 |
71 | for Tool, Events in Modes do
72 | local Event = Events[Name]
73 | local Callback = OnRecieve(Tool)
74 |
75 | if Tool == "warp" then
76 | Event:Connect(Callback)
77 | continue
78 | elseif Tool == "roblox" then
79 | Event.OnServerEvent:Connect(Callback)
80 | continue
81 | end
82 |
83 | local Method = Event.On or Event.SetCallback or Event.listen
84 | Method(Callback)
85 | end
86 | end
87 |
88 | ReplicatedStorage.Shared.GetRecieved.OnServerInvoke = function()
89 | return Recieved
90 | end
91 |
92 | ReplicatedStorage.Shared.Generate.OnServerEvent:Connect(function(Player, JSON)
93 | local OutputJSON = Instance.new("StringValue")
94 | OutputJSON.Name = "Result"
95 | OutputJSON.Value = JSON
96 | OutputJSON.Parent = game
97 | print("Generated results")
98 | end)
99 |
--------------------------------------------------------------------------------
/benchmark/src/shared/benches/Booleans.luau:
--------------------------------------------------------------------------------
1 | return table.create(1000, true)
--------------------------------------------------------------------------------
/benchmark/src/shared/benches/Entities.luau:
--------------------------------------------------------------------------------
1 | local Array = {}
2 | for Index = 1, 100 do
3 | table.insert(Array, {
4 | id = math.random(1, 255),
5 | x = math.random(1, 255),
6 | y = math.random(1, 255),
7 | z = math.random(1, 255),
8 | orientation = math.random(1, 255),
9 | animation = math.random(1, 255),
10 | })
11 | end
12 | return Array
--------------------------------------------------------------------------------
/benchmark/src/shared/benches/init.luau:
--------------------------------------------------------------------------------
1 | local Benches = {}
2 | for Index, Bench in script:GetChildren() do
3 | --> Reset random seed to a constant value
4 | --> This results in the same values being generated on both server and client
5 | math.randomseed(0)
6 | Benches[Bench.Name] = require(Bench)
7 | end
8 | return Benches
9 |
--------------------------------------------------------------------------------
/benchmark/src/shared/modes/blink.luau:
--------------------------------------------------------------------------------
1 | local RunService = game:GetService("RunService")
2 | if RunService:IsServer() then
3 | return require("../blink/Server")
4 | elseif RunService:IsClient() then
5 | return require("../blink/Client")
6 | end
--------------------------------------------------------------------------------
/benchmark/src/shared/modes/bytenet.luau:
--------------------------------------------------------------------------------
1 | local ByteNet = require("../../../packages/bytenet")
2 | return ByteNet.defineNamespace("Benchmark", function()
3 | return {
4 | Booleans = ByteNet.definePacket({
5 | value = ByteNet.array(ByteNet.bool)
6 | }),
7 | Entities = ByteNet.definePacket({
8 | value = ByteNet.array(ByteNet.struct({
9 | id = ByteNet.uint8,
10 | x = ByteNet.uint8,
11 | y = ByteNet.uint8,
12 | z = ByteNet.uint8,
13 | orientation = ByteNet.uint8,
14 | animation = ByteNet.uint8
15 | }))
16 | })
17 | }
18 | end)
--------------------------------------------------------------------------------
/benchmark/src/shared/modes/init.luau:
--------------------------------------------------------------------------------
1 | local DISABLED_MODES = {
2 | warp = true
3 | }
4 |
5 | local Modes = {}
6 | for Index, Mode in script:GetChildren() do
7 | if DISABLED_MODES[Mode.Name] then
8 | continue
9 | end
10 |
11 | Modes[Mode.Name] = require(Mode)
12 | end
13 |
14 | return Modes
15 |
--------------------------------------------------------------------------------
/benchmark/src/shared/modes/roblox.luau:
--------------------------------------------------------------------------------
1 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
2 | local RunService = game:GetService("RunService")
3 |
4 | local Benches = require("../benches")
5 |
6 | local Events = {}
7 | local IsServer = RunService:IsServer()
8 |
9 | for Name in Benches do
10 | local Event: RemoteEvent;
11 | if IsServer then
12 | Event = Instance.new("RemoteEvent")
13 | Event.Name = Name
14 | Event.Parent = ReplicatedStorage
15 | else
16 | Event = ReplicatedStorage:WaitForChild(Name)
17 | end
18 |
19 | Events[Name] = Event
20 | end
21 |
22 | return Events
23 |
--------------------------------------------------------------------------------
/benchmark/src/shared/modes/warp.luau:
--------------------------------------------------------------------------------
1 | local RunService = game:GetService("RunService")
2 |
3 | local Warp = require("../../../packages/warp")
4 | local Benches = require("../benches")
5 |
6 | local Events = {}
7 | local Method = RunService:IsServer() and "Server" or "Client"
8 |
9 | for Name in Benches do
10 | Events[Name] = Warp[Method](Name, {
11 | interval = 0,
12 | maxEntrance = math.huge,
13 | })
14 | end
15 |
16 | return Events
17 |
--------------------------------------------------------------------------------
/benchmark/src/shared/modes/zap.luau:
--------------------------------------------------------------------------------
1 | local RunService = game:GetService("RunService")
2 | if RunService:IsServer() then
3 | return require("../zap/Server")
4 | elseif RunService:IsClient() then
5 | return require("../zap/Client")
6 | end
--------------------------------------------------------------------------------
/build/.darklua.json:
--------------------------------------------------------------------------------
1 | {
2 | "bundle": {
3 | "excludes": [
4 | "@lune/**"
5 | ],
6 | "modules_identifier": "__DARKLUA_BUNDLE_MODULES",
7 | "require_mode": {
8 | "name": "path",
9 | "sources": {
10 | "Base": "../src/Templates/Base.txt",
11 | "Client": "../src/Templates/Client.txt",
12 | "Server": "../src/Templates/Server.txt"
13 | }
14 | }
15 | },
16 | "generator": "readable",
17 | "rules": [
18 | "remove_types",
19 | "compute_expression",
20 | "remove_unused_if_branch",
21 | {
22 | "identifier": "BUNDLED",
23 | "rule": "inject_global_value",
24 | "value": true
25 | },
26 | {
27 | "identifier": "VERSION",
28 | "rule": "inject_global_value",
29 | "value": "0.17.3"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/build/build.bat:
--------------------------------------------------------------------------------
1 | @echo Off
2 | SET ZIP="C:\Program Files\7-Zip\7z.exe"
3 | SET /p VERSION=What version is being built?:
4 |
5 | echo Clearing release folder
6 | del /s /q "../release"
7 | rmdir /s/q "../Blink"
8 | mkdir "../release"
9 | mkdir "../Blink"
10 |
11 | lune run version.luau %VERSION%
12 |
13 | echo Bundling source code
14 | darklua process ../src/CLI/init.luau ./Bundled.luau
15 |
16 | echo Building standalone executable
17 |
18 | lune build ./Bundled.luau --output blink --target windows-x86_64
19 | %ZIP% a "../release/blink-%VERSION%-windows-x86_64.zip" "blink.*" > nul
20 | del "./blink.*"
21 |
22 | lune build ./Bundled.luau --output blink --target macos-x86_64
23 | %ZIP% a "../release/blink-%VERSION%-macos-x86_64.zip" "blink" > nul
24 | del "./blink.*"
25 |
26 | lune build ./Bundled.luau --output blink --target linux-x86_64
27 | %ZIP% a "../release/blink-%VERSION%-linux-x86_64.zip" "blink" > nul
28 | del "./blink.*"
29 |
30 | lune build ./Bundled.luau --output blink --target macos-aarch64
31 | %ZIP% a "../release/blink-%VERSION%-macos-aarch64.zip" "blink" > nul
32 | del "./blink.*"
33 |
34 | lune build ./Bundled.luau --output blink --target linux-aarch64
35 | %ZIP% a "../release/blink-%VERSION%-linux-aarch64.zip" "blink" > nul
36 | del "./blink.*"
37 |
38 | echo Compiling bytecode
39 | lune run compile.luau
40 |
41 | echo Zipping files
42 | %ZIP% a "../release/bytecode.zip" "Bytecode.txt" > nul
43 | %ZIP% a "../release/bytecode.zip" "init.luau" > nul
44 |
45 | echo Packaging plugin
46 | cd ../plugin
47 | mkdir "./bundle"
48 | copy ".\src\Error.rbxmx" ".\bundle\Error.rbxmx"
49 | copy ".\src\Widget.rbxmx" ".\bundle\Widget.rbxmx"
50 | copy "..\build\.darklua.json" ".\.darklua.json"
51 | darklua process "./src/init.server.luau" "./bundle/init.server.lua"
52 | rojo build bundle.project.json --output "../release/blink-%VERSION%-plugin.rbxm"
53 |
54 | cd ../build
55 |
56 | del "./Bytecode.txt"
57 | rmdir /s/q "../Blink"
58 |
59 | pause
--------------------------------------------------------------------------------
/build/compile.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 | local luau = require("@lune/luau")
3 |
4 | local Source = fs.readFile("./Bundled.luau")
5 | Source = ("--!native\n\n" .. Source)
6 | fs.removeFile("./Bundled.luau")
7 | fs.writeFile("./Bytecode.txt",
8 | luau.compile(Source, {
9 | optimizationLevel = 2
10 | })
11 | )
--------------------------------------------------------------------------------
/build/init.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | --!optimize 2
3 |
4 | local fs = require("@lune/fs")
5 | local luau = require("@lune/luau")
6 |
7 | local Bytecode = fs.readFile("./Bytecode.txt")
8 | luau.load(Bytecode)()
--------------------------------------------------------------------------------
/build/version.luau:
--------------------------------------------------------------------------------
1 | local fs = require("@lune/fs")
2 | local net = require("@lune/net")
3 | local process = require("@lune/process")
4 |
5 | local DarkluaConfig = fs.readFile("./.darklua.json")
6 | local JsonConfig = net.jsonDecode(DarkluaConfig)
7 |
8 | local Version = process.args[1]
9 |
10 | local Rules = JsonConfig.rules
11 | local VersionGlobal = Rules[5]
12 |
13 | if VersionGlobal.identifier ~= "VERSION" then
14 | error("Rules were shifted, update the build script.")
15 | end
16 |
17 | VersionGlobal.value = Version
18 | fs.writeFile("./.darklua.json", net.jsonEncode(JsonConfig, true))
19 | print(`Updated darklua config version to {Version}`)
--------------------------------------------------------------------------------
/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Blink",
3 | "tree": {
4 | "$path": "plugin/src"
5 | }
6 | }
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | /.DS_Store
2 | /node_modules
3 | /temp
4 | /out
5 | /.next
6 | /.env
7 |
8 | /**/.DS_Store
9 | /**/node_modules
10 | /**/temp
11 | /**/out
12 | /**/.next
13 | /**/.env
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Nextra Docs Template
2 |
3 | This is a template for creating documentation with [Nextra](https://nextra.site).
4 |
5 | [**Live Demo →**](https://nextra-docs-template.vercel.app)
6 |
7 | [](https://nextra-docs-template.vercel.app)
8 |
9 | ## Quick Start
10 |
11 | Click the button to clone this repository and deploy it on Vercel:
12 |
13 | [](https://vercel.com/new/clone?s=https%3A%2F%2Fgithub.com%2Fshuding%2Fnextra-docs-template&showOptionalTeamCreation=false)
14 |
15 | ## Local Development
16 |
17 | First, run `pnpm i` to install the dependencies.
18 |
19 | Then, run `pnpm dev` to start the development server and visit localhost:3000.
20 |
21 | ## License
22 |
23 | This project is licensed under the MIT License.
24 |
--------------------------------------------------------------------------------
/docs/components/counters.module.css:
--------------------------------------------------------------------------------
1 | .counter {
2 | border: 1px solid #ccc;
3 | border-radius: 5px;
4 | padding: 2px 6px;
5 | margin: 12px 0 0;
6 | }
7 |
--------------------------------------------------------------------------------
/docs/components/counters.tsx:
--------------------------------------------------------------------------------
1 | // Example from https://beta.reactjs.org/learn
2 |
3 | import { useState } from 'react'
4 | import styles from './counters.module.css'
5 |
6 | function MyButton() {
7 | const [count, setCount] = useState(0)
8 |
9 | function handleClick() {
10 | setCount(count + 1)
11 | }
12 |
13 | return (
14 |
15 |
18 |
19 | )
20 | }
21 |
22 | export default function MyApp() {
23 | return
24 | }
25 |
--------------------------------------------------------------------------------
/docs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/docs/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs'
2 | import nextra from 'nextra'
3 | import { BUNDLED_LANGUAGES, getHighlighter } from 'shiki'
4 |
5 | const withNextra = nextra({
6 | theme: 'nextra-theme-docs',
7 | themeConfig: './theme.config.tsx',
8 | mdxOptions: {
9 | rehypePrettyCodeOptions: {
10 | theme: JSON.parse(
11 | readFileSync('./public/syntax/mocha.json', 'utf8')
12 | ),
13 | getHighlighter: options =>
14 | getHighlighter({
15 | ...options,
16 | langs: [
17 | ...BUNDLED_LANGUAGES,
18 | {
19 | id: 'blink',
20 | scopeName: 'source.blink',
21 | aliases: [],
22 | path: '../../public/syntax/blink.tmLanguage.json'
23 | }
24 | ]
25 | })
26 | }
27 | }
28 | })
29 |
30 | export default withNextra({
31 | output: "export",
32 | basePath: "/blink",
33 | images: {unoptimized: true}
34 | })
35 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextra-docs-template",
3 | "version": "0.0.1",
4 | "description": "Nextra docs template",
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/shuding/nextra-docs-template.git"
13 | },
14 | "author": "Shu Ding ",
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/shuding/nextra-docs-template/issues"
18 | },
19 | "homepage": "https://github.com/shuding/nextra-docs-template#readme",
20 | "dependencies": {
21 | "next": "^14.2.21",
22 | "nextra": "^2.13.4",
23 | "nextra-theme-docs": "^2.13.4",
24 | "react": "^18.3.1",
25 | "react-dom": "^18.3.1"
26 | },
27 | "devDependencies": {
28 | "@types/node": "18.11.10",
29 | "typescript": "^4.9.3"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/pages/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": "Home",
3 | "getting-started": "Getting Started",
4 | "language": "Blink's Language"
5 | }
6 |
--------------------------------------------------------------------------------
/docs/pages/getting-started/1-installation.mdx:
--------------------------------------------------------------------------------
1 | import { Callout, Steps } from 'nextra/components'
2 |
3 | # Installation
4 | The recommened way to install blink is using [Rokit](https://github.com/rojo-rbx/rokit).
5 |
6 |
7 | ### Installing Blink
8 |
9 | Open a terminal in your project directory and run the following command:
10 |
11 | ```sh copy filename="Bash"
12 | rokit add 1Axen/blink
13 | ```
14 |
15 | ### Updating Blink
16 |
17 | When a new version of blink releases, using Rokit you can easily update using the following command:
18 |
19 | ```sh copy filename="Bash"
20 | rokit update 1Axen/blink
21 | ```
22 |
23 |
24 |
25 | ## Alternatives
26 |
27 |
28 | GitHub Releases
29 |
30 | You can download pre-built binaries directly from the
31 | [GitHub Releases](https://github.com/1Axen/blink/releases) page.
32 |
33 |
34 |
35 |
36 | pesde (0.15.0+)
37 |
38 | An unofficial release is available on pesde starting from version 0.15.0.
39 | To install blink using pesde, open a terminal in your project directory and run the following commands:
40 | ```sh copy filename="Bash"
41 | pesde add --dev pesde/blink
42 | pesde install
43 | ```
44 |
45 |
46 |
47 |
48 | Roblox Studio
49 | Blink offers a companion studio plugin which allows you to write and generate files within Studio without the need for external tooling.
50 | The studio plugin can be installed from the [Creator Store](https://create.roblox.com/store/asset/77231976488966/Blink-Editor).
51 | Alternatively you can download it from [Github Releases](https://github.com/1Axen/Blink/releases).
52 |
53 |
54 |
55 | Bytecode
56 |
57 | Blink releases also contain bundled bytecode which can be used to run blink.
58 |
59 |
60 | ### Downloading Bytecode
61 | You can download the bundled bytecode from [Github Releases](https://github.com/1Axen/Blink/releases).
62 | Once downloaded unzip it into your target directory.
63 |
64 | ### Installing Lune
65 | Blink uses lune as it's runtime enviornment, you can install lune using Rokit:
66 | ```sh copy filename="Bash"
67 | rokit add lune
68 | ```
69 |
70 | ### Running bytecode
71 | Open the directory in which you unzipped the bytecode and run the following command:
72 | ```sh copy filename="Bash"
73 | lune run init [INPUT] -- [ARGS]
74 | ```
75 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/pages/getting-started/2-introduction.mdx:
--------------------------------------------------------------------------------
1 | import { Callout, Steps } from 'nextra/components'
2 |
3 | # Introduction
4 |
5 | This guide is aimed towards CLI users, any syntax will apply to the Studio plugin as well.
6 |
7 | ## First Steps
8 |
9 |
10 |
11 | ### Writing Your First Network Description
12 |
13 | Blink's language is really simple, you will learn more throught the guide.
14 | First create a `.blink` file in your desired directory, then copy the example below into your file:
15 |
16 | ```blink copy
17 | -- these can be ignored if you are using the studio plugin
18 | option ClientOutput = "path/to/client.luau"
19 | option ServerOutput = "path/to/server.luau"
20 |
21 | event MyFirstEvent {
22 | from: Server,
23 | type: Reliable,
24 | call: SingleSync,
25 | data: string
26 | }
27 | ```
28 |
29 | ### Compile Your Network Description
30 |
31 | Open a terminal in the directory you created your file and run the following command:
32 |
33 | ```sh copy
34 | blink FILE_NAME
35 | ```
36 |
37 | This will generate 2 Luau files, "path/to/client.luau" and "path/to/server.luau".
38 | You can now use these files in your project.
39 |
40 | ### Using The Generated Code
41 |
42 | Blink returns an immutable table with all of your events and functions.
43 | Using this table you can connect to your events and or functions and fire/invoke them.
44 |
45 | ```lua copy filename="server" {3}
46 | local Net = require(Path.To.Server)
47 |
48 | Net.MyFirstEvent.FireAll("Hello World")
49 | ```
50 |
51 | ```lua copy filename="client" {3-5}
52 | local Net = require(Path.To.Client)
53 |
54 | Net.MyFirstEvent.On(function(Text)
55 | print(Text)
56 | end)
57 | ```
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/docs/pages/getting-started/3-cli.mdx:
--------------------------------------------------------------------------------
1 | # Command-Line Usage
2 | ## Compiling Descriptions
3 | Once you have written your description file (ex. `file-name.blink`), you can compile it like this:
4 |
5 | ```sh copy
6 | blink file-name
7 | ```
8 |
9 | This will look for a file `file-name` with an extension `.blink` or `.txt`, in the current directory.
10 | ### Watching Description Files
11 | When you're making many rapid changes to a description file or it's imports it can be tedious to constantly have to re-run the build command.
12 | Blink offers a solution in the form of a `--watch` option, when passed the target file and all it's imports (including imports of imports and so on) will be watched for changes,
13 | recompiling automatically when one occurs.
14 | ```sh copy
15 | blink file-name --watch
16 | ```
--------------------------------------------------------------------------------
/docs/pages/getting-started/4-plugin.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # Studio Plugin
4 |
5 |
6 | ## Navigating
7 | After installing the plugin locate it within your plugin tab in Studio.
8 |
9 | After opening the plugin you will be prompted to give it access to inject scripts, this is needed to be able to generate output files.
10 |
11 |
12 | 
13 |
14 | ## Side Menu
15 | You can open the side menu using the sandwich button on the left hand side.
16 | 
17 |
18 | Within the side menu you can manage your network description files.
19 | 
20 |
21 | ## Saving
22 | To save a network description simply press the save button at the bottom of the side menu.
23 | This will prompt you to save whatever is currently in the editor.
24 |
25 |
26 | You can save to already existing files by simply inputting their name.
27 |
28 |
29 | 
30 |
31 | ## Generating
32 | To generate your networking modules simply press the "Generate" button on your desired file.
33 | This will open a prompt asking you to select your desired output destination within the game explorer.
34 |
35 |
36 | Make sure to select a location that is accessible to both the client and server, otherwise you won't be able to require the generated files.
37 |
38 |
39 | 
40 |
41 | Once you've selected your desired output destination simply press "Generate" and your files will be ready.
42 |
43 | 
44 |
45 | If no issues arise Blink will generate the following Folder containing your networking modules:
46 |
47 | 
48 |
49 | ## Editor
50 | The editor is the center piece of the plugin, it has various rudimentary intellisense features built-in to help with your writing speed, and analysis to correct any mistakes.
51 | 
52 |
53 | ## Errors
54 | Upon generating output files, Blink will parse the source contents and inform you of any errors within your files that are blocking generation.
55 | 
--------------------------------------------------------------------------------
/docs/pages/getting-started/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "1-installation": "Installation",
3 | "2-introduction": "Introduction",
4 | "3-cli": "Command-Line Usage",
5 | "4-plugin": "Roblox Studio Plugin"
6 | }
--------------------------------------------------------------------------------
/docs/pages/index.mdx:
--------------------------------------------------------------------------------
1 | # Blink
2 | An IDL compiler written in Luau for ROBLOX buffer networking
3 |
4 | ## Performance
5 | Blink aims to generate the most performant and bandwidth-efficient code for your specific experience, but what does this mean?
6 |
7 | It means lower bandwidth usage directly resulting in **lower ping\*** experienced by players and secondly, it means **lower CPU usage** compared to more generalized networking solutions.
8 |
9 | *\* In comparison to standard ROBLOX networking, this may not always be the case but should never result in increased ping times.*
10 |
11 | Benchmarks are available here [here](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md).
12 |
13 | ## Security
14 | Blink does two things to combat bad actors:
15 | 1. Data sent by clients will be **validated** on the receiving side before reaching any critical game code.
16 | 2. As a result of the compression done by Blink it becomes **significantly harder** to snoop on your game's network traffic. Long gone are the days of skids using RemoteSpy to snoop on your game's traffic.
17 |
18 | ## Get Started
19 | Head over to the [installation](getting-started/1-installation.mdx) page to get started with Blink!
--------------------------------------------------------------------------------
/docs/pages/language/1-options.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # Options
4 | Options go at the top of a source file, and allow you to configure the output of Blink.
5 | ```blink copy
6 | option [OPTION] = [VALUE]
7 | ```
8 |
9 | ## `Casing`
10 | Default: `Pascal`
11 | Options: `Pascal`, `Camel`, `Snake`
12 |
13 | Controls the casing with which event/function methods generate.
14 | ```blink copy
15 | option Casing = Camel
16 | ```
17 |
18 | ## `ServerOutput`, `ClientOutput`, `TypesOutput`
19 | These options allow you to specify where Blink will generate the respective output files.
20 | ```blink copy
21 | option TypesOutput = "../Network/Types.luau"
22 | option ServerOutput = "../Network/Server.luau"
23 | option ClientOutput = "../Network/Client.luau"
24 | ```
25 |
26 | ## `Typescript`
27 | Default: `false`
28 |
29 | Tells Blink whether to generate TypeScript definition files alongside Luau files.
30 | The generated `d.ts` files are placed in the same path as your output files.
31 | ```blink copy
32 | option Typescript = true
33 | ```
34 |
35 | ## `UsePolling`
36 | Default: `false`
37 |
38 | Instructs the compiler to automatically output all events with a polling API.
39 | ```blink copy
40 | option UsePolling = true
41 | ```
42 |
43 | ## `FutureLibrary` and `PromiseLibrary`
44 | In order to use future and promise yield types with functions a path to each library used must be specified
45 | ```blink copy
46 | option FutureLibrary = "ReplicatedStorage.Packages.Future"
47 | option PromiseLibrary = "ReplicatedStorage.Packages.Promise"
48 | ```
49 |
50 | ## `SyncValidation`
51 | Default: `false`
52 |
53 | Controls if Blink will check whether a sync call yielded.
54 |
55 | ## `WriteValidations`
56 | Default: `false`
57 |
58 | Controls if Blink will check types when writing them (firing an event/invoking a function). Helpful for debugging and during development, but it might result in degraded performance. It is encouraged you disable this option in production.
59 |
60 | Blink only checks for builtin primitives. For example if a number was passed. More complicated types like structs, maps and enums cannot be validated.
61 |
62 |
63 | ## `ManualReplication`
64 | Default: `false`
65 |
66 | Controls if Blink will replicate events and functions automatically at the end of every frame.
67 | When set to `true` automatic replication will be disabled and a `StepReplication` function will be exposed instead.
68 |
69 | ## `RemoteScope`
70 | Default: `""`
71 |
72 | Adds a prefix to the events generated by this Blink instance. For example a value of `"PACKAGE"` creates events `"PACKAGE_BLINK_RELIABLE_REMOTE"` and `"PACKAGE_BLINK_UNRELIABLE_REMOTE"`.
73 | If the option is not specified, they will just be `"BLINK_RELIABLE_REMOTE"` and `"BLINK_UNRELIABLE_REMOTE"`.
74 | This is particularly useful if you're using Blink inside of a package, and don't want to interfere with a game's own Blink setup.
--------------------------------------------------------------------------------
/docs/pages/language/2-scopes.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # Scopes
4 | Scopes allow you to group similiar types together for better per-file organization.
5 |
6 | ## Defining Scopes
7 | You can define a scope using the `scope` keyword followed by the scope name.
8 | ```blink copy
9 | scope ExampleScope {
10 | type InScopeType = u8
11 | event InScopeEvent {
12 | From: Server,
13 | Type: Reliable,
14 | Call: SingleSync,
15 | Data: u8
16 | }
17 | }
18 |
19 | struct Example {
20 | Reference = ExampleScope.InScopeType
21 | }
22 | ```
23 |
24 | Scopes automatically capture any definitions within their parent scopes.
25 | ```blink copy
26 | type Outer = u8
27 | scope CaptureExample {
28 | type Inner = Outer
29 | }
30 | ```
31 |
32 | ## Usage in Luau code
33 | Whenever a type or event/function is defined within a scope, their export is nested within the scope's table, and their luau type is prefixed with the scope's name.
34 | ```lua copy
35 | local Blink = require(PATH_TO_BLINK)
36 | Blink.ExampleScope.InScopeEvent.FireAll(0)
37 |
38 | local Number: Blink.ExampleScope_InScopeType = 0
39 | ```
--------------------------------------------------------------------------------
/docs/pages/language/3-imports.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # Imports
4 | Imports allow you to split your blink config into multiple files for better organization.
5 |
6 | ## Importing Files
7 | You can import another blink file using the `import` keyword followed by a file system path to the target file.
8 | Imported files are placed in a new scope, using the file name or a user provided name through the `as` keyword.
9 |
10 | Imports also work in the Roblox Studio editor, but only support sibling `(./)` imports.
11 |
12 | ```blink copy
13 | import "./external"
14 | import "./external" as "Common"
15 |
16 | type ExternType = external.Type
17 | type CommonType = Common.Type
18 | ```
19 | ## Usage in Luau code
20 | Since imports act as scopes, the same rules apply to them as well.
21 | ```lua copy
22 | local Blink = require(PATH_TO_BLINK)
23 | Blink.external.Event.FireAll(0)
24 |
25 | local Number: Blink.Common_Type = 0
26 | ```
--------------------------------------------------------------------------------
/docs/pages/language/5-events.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # Events
4 | Events are Blink's version of Roblox's `RelibaleEvent` and `UnreliableEvent`.
5 | They are the main way to communicate between client and server.
6 |
7 | ## Usage in Blink
8 |
9 | Events can be defined using the `event` keyword.
10 | ```blink copy
11 | event MyEvent {
12 | From: Server,
13 | Type: Reliable,
14 | Call: SingleAsync,
15 | Data: f64
16 | }
17 | ```
18 |
19 | #### `From`
20 | Determines the side from which the event is fired.
21 | Currently Blink supports either `Server` or `Client`.
22 |
23 | #### `Type`
24 | Determines the type (reliability) of the event.
25 | * `Reliable` - Events are guaranteed to arrive at their destination in the order they were sent in.
26 | * `Unreliable` - Events are not guaranteed to arrive at their destination or to arrive in the order they were sent in. They also have a maximum size of 1000 bytes.
27 |
28 | #### `Call`
29 | Determines the listening API exposed on the receiving side.
30 | * `SingleSync` - Events can only have one listener, but that listener cannot yield.
31 | * `ManySync` - Events can have many listeners, but those listeners cannot yield.
32 | * `SingleAsync` - Events can only have one listener, and that listener may yield.
33 | * `ManyAsync` - Events can have many listeners, and those listeners may yield.
34 | * `Polling` - Events are iterated through `Event.Iter()`.
35 |
36 |
37 | Sync events should be avoided unless performance is critical.
38 | Yielding or erroring in sync event can cause undefined and sometimes game-breaking behaviour.
39 |
40 |
41 | #### `Data`
42 | Determines the data that is sent through the event, can be any [type](./4-types.mdx).
43 | This field can be omitted if no data is required.
44 |
45 | ##### Type Packs
46 | Multiple data values are supported through the usage of a type pack (commonly referred to as a tuple).
47 | Type packs can be defined as a list of [types](./4-types.mdx) seperated by a comma within parenthesis.
48 | For example, a type pack of different [number types](./4-types#numbers) can be written like so:
49 | ```blink copy
50 | event MyTypePackEvent {
51 | From: Server,
52 | Type: Reliable,
53 | Call: SingleAsync,
54 | Data: (u8, u16, u32)
55 | }
56 | ```
57 |
58 | ## Usage in Luau
59 |
60 | ### Firing an Event
61 | ```lua filename="client.luau" copy
62 | blink.MyEvent.Fire(5)
63 | blink.MyTypePackEvent.Fire(2^8 - 1, 2^16 - 1, 2^32 - 1)
64 | ```
65 | ```lua filename="server.luau" copy
66 | blink.MyEvent.Fire(Player, 5)
67 | blink.MyEvent.FireAll(Player, 5)
68 | blink.MyEvent.FireList((Player), 5)
69 | blink.MyEvent.FireExcept(Player, 5)
70 | ```
71 |
72 | ### Listening to an Event
73 | ```lua filename="client.luau" copy
74 | blink.MyEvent.On(function(Value)
75 | -- ...
76 | end)
77 |
78 | blink.MyTypePackEvent.On(function(Foo, Bar, FooBar)
79 | -- ...
80 | end)
81 | ```
82 | ```lua filename="server.luau" copy
83 | blink.MyEvent.On(function(Player, Value)
84 | -- ...
85 | end)
86 |
87 | blink.MyTypePackEvent.On(function(Player, Foo, Bar, FooBar)
88 | -- ...
89 | end)
90 | ```
91 | ```lua filename="disconnect.luau" copy
92 | local Disconnect = blink.MyEvent.On(...)
93 | Disconnect()
94 | ```
95 |
96 | ### Iterating an Event (Polling)
97 | ```lua filename="client.luau" copy
98 | for Index, Value in MyEvent.Iter() do
99 | -- ...
100 | end
101 | for Index, Foo, Bar, FooBar in MyTypePackEvent.Iter() do
102 | -- ...
103 | end
104 | ```
105 | ```lua filename="server.luau" copy
106 | for Index, Player, Value in MyEvent.Iter() do
107 | -- ...
108 | end
109 | for Index, Player, Foo, Bar, FooBar in MyTypePackEvent.Iter() do
110 | -- ...
111 | end
112 | ```
--------------------------------------------------------------------------------
/docs/pages/language/6-functions.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # Functions
4 | Functions are Blink's version of Roblox's `RemoteFunction`.
5 | They provide a way for the client to request information from the server.
6 |
7 | ## Usage in Blink
8 |
9 | Functions can be defined using the `function` keyword.
10 | ```blink copy
11 | function MyFunction {
12 | Yield: Coroutine,
13 | Data: f64,
14 | Return: f64
15 | }
16 | ```
17 |
18 | #### `Yield`
19 | Determines the library used to handle yielding to the requester.
20 | * `Coroutine` - The builtin Luau coroutine library.
21 | * `Future` - The user provided future library, use of redblox's future library is recommended.
22 | * `Promise` - The user provided promise library, use of evaera's promise library (or forks of it) is recommended.
23 |
24 | #### `Data`
25 | The data sent to the server by the client.
26 | For more information take a look at the [data field](./5-events.mdx#data) for events.
27 |
28 | #### `Return`
29 | The data returned by the server to the client.
30 | For more information take a look at the [data field](./5-events.mdx#data) for events.
31 |
32 | ## Usage in Luau
33 |
34 | ### Invoking a Function
35 | ```lua filename="client-coroutine.luau" copy
36 | local Value = blink.MyFunction.Invoke(5)
37 | ```
38 | ```lua filename="client-future.luau" copy
39 | local Future = blink.MyFunction.Invoke(5)
40 | local Value = Future:Await()
41 | ```
42 | ```lua filename="client-promise.luau" copy
43 | local Promise = blink.MyFunction.Invoke(5)
44 | local Value = Promise:await()
45 | ```
46 |
47 | ### Listening to a Function
48 | ```lua filename="server.luau" copy
49 | blink.MyFunction.On(function(Player, Value)
50 | return Value * 2
51 | end)
52 | ```
--------------------------------------------------------------------------------
/docs/pages/language/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "1-options": "Options",
3 | "2-scopes": "Scopes",
4 | "3-imports": "Imports",
5 | "4-types": "Types",
6 | "5-events": "Events",
7 | "6-functions": "Functions"
8 | }
--------------------------------------------------------------------------------
/docs/public/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/Logo.png
--------------------------------------------------------------------------------
/docs/public/letter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/letter.png
--------------------------------------------------------------------------------
/docs/public/plugin/Duplicate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Duplicate.png
--------------------------------------------------------------------------------
/docs/public/plugin/Editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Editor.png
--------------------------------------------------------------------------------
/docs/public/plugin/Error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Error.png
--------------------------------------------------------------------------------
/docs/public/plugin/Generate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Generate.png
--------------------------------------------------------------------------------
/docs/public/plugin/GenerateSelected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/GenerateSelected.png
--------------------------------------------------------------------------------
/docs/public/plugin/Invalid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Invalid.png
--------------------------------------------------------------------------------
/docs/public/plugin/Locate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Locate.png
--------------------------------------------------------------------------------
/docs/public/plugin/Menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Menu.png
--------------------------------------------------------------------------------
/docs/public/plugin/Navigation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Navigation.png
--------------------------------------------------------------------------------
/docs/public/plugin/Output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Output.png
--------------------------------------------------------------------------------
/docs/public/plugin/Save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1Axen/blink/d70bcb08f31c4800e8f19c7123435b43d005f713/docs/public/plugin/Save.png
--------------------------------------------------------------------------------
/docs/public/syntax/LICENSE:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright © `2024` `VirtualButFake (Tijn Epema)`\
4 | Copyright © `2024` `checkraisefold`
5 |
6 | Permission is hereby granted, free of charge, to any person
7 | obtaining a copy of this software and associated documentation
8 | files (the “Software”), to deal in the Software without
9 | restriction, including without limitation the rights to use,
10 | copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the
12 | Software is furnished to do so, subject to the following
13 | conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 | OTHER DEALINGS IN THE SOFTWARE.
26 |
27 | MIT License
28 |
29 | Copyright (c) 2021 Catppuccin
30 |
31 | Permission is hereby granted, free of charge, to any person obtaining a copy
32 | of this software and associated documentation files (the "Software"), to deal
33 | in the Software without restriction, including without limitation the rights
34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35 | copies of the Software, and to permit persons to whom the Software is
36 | furnished to do so, subject to the following conditions:
37 |
38 | The above copyright notice and this permission notice shall be included in all
39 | copies or substantial portions of the Software.
40 |
41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47 | SOFTWARE.
48 |
--------------------------------------------------------------------------------
/docs/public/syntax/blink.tmLanguage.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3 | "name": "Blink",
4 | "scopeName": "source.blink",
5 | "patterns": [
6 | { "include": "#comments" },
7 | { "include": "#scope" },
8 | { "include": "#brackets" },
9 | { "include": "#autoClosingPairs" },
10 | { "include": "#surroundingPairs" },
11 | { "include": "#typeKeywords" },
12 | { "include": "#keywords" },
13 | { "include": "#types" },
14 | { "include": "#setting" },
15 | { "include": "#operators" },
16 | { "include": "#symbols" },
17 | { "include": "#tokenizer" }
18 | ],
19 | "repository": {
20 | "comments": {
21 | "patterns": [
22 | {
23 | "begin": "--\\[([=]*)\\[",
24 | "end": "\\]([=]*)\\]",
25 | "captures": {
26 | "0": { "name": "comment.block.documentation.blink" },
27 | "1": { "name": "comment.block.documentation.blink" }
28 | },
29 | "name": "comment.block.documentation.blink"
30 | },
31 | {
32 | "match": "--.*$",
33 | "name": "comment.line.double-dash.blink"
34 | }
35 | ]
36 | },
37 | "scope": {
38 | "patterns": [
39 | {
40 | "begin": "\\b(scope)\\b\\s+([a-zA-Z_]*)\\b\\s+(\\{)",
41 | "end": "(\\})",
42 | "beginCaptures": {
43 | "1": { "name": "keyword.control.blink" },
44 | "2": { "name": "storage.type.namespace.blink" },
45 | "3": { "name": "punctuation.section.braces.blink" }
46 | },
47 | "endCaptures": { "1": { "name": "punctuation.section.braces.blink" } },
48 | "patterns": [ { "include": "source.blink" } ]
49 | }
50 | ]
51 | },
52 | "brackets": {
53 | "patterns": [
54 | {
55 | "begin": "\\{",
56 | "end": "\\}",
57 | "name": "punctuation.section.braces.blink",
58 | "patterns": [
59 | { "include": "#types" },
60 | { "include": "#typeKeywords" },
61 | { "include": "#operators" },
62 | { "include": "#setting" },
63 | { "include": "#symbols" },
64 | { "include": "#comments" },
65 | { "include": "#brackets" },
66 | {
67 | "match": "([a-zA-Z_]\\w*):",
68 | "captures": { "1": { "name": "variable.other.blink" } }
69 | },
70 | {
71 | "match": "([a-zA-Z_]\\w*)\\s*,",
72 | "captures": {
73 | "1": { "name": "variable.other.blink" },
74 | "2": { "name": "variable.other.blink" }
75 | },
76 | "name": "meta.array-assignment.blink"
77 | },
78 | {
79 | "match": "([a-zA-Z_]\\w*)\\s*",
80 | "captures": { "1": { "name": "variable.other.blink" } },
81 | "name": "meta.array-assignment.blink"
82 | }
83 | ]
84 | },
85 | {
86 | "begin": "\\[",
87 | "end": "\\]",
88 | "name": "punctuation.section.brackets.blink",
89 | "patterns": [
90 | { "include": "#types" },
91 | { "include": "#numbers" },
92 | { "include": "#delimiters" },
93 | { "include": "#strIdentifiers" },
94 | {
95 | "match": "([a-zA-Z_]\\w*)",
96 | "captures": { "1": { "name": "variable.other.blink" } }
97 | }
98 | ]
99 | },
100 | {
101 | "begin": "\\(",
102 | "end": "\\)",
103 | "name": "punctuation.section.parens.blink",
104 | "patterns": [
105 | { "include": "#numbers" },
106 | { "include": "#delimiters" },
107 | { "include": "#strIdentifiers" },
108 | { "include": "#types" }
109 | ]
110 | },
111 | {
112 | "begin": "<",
113 | "end": ">",
114 | "name": "punctuation.section.chevrons.blink",
115 | "patterns": [
116 | { "include": "#numbers" },
117 | { "include": "#delimiters" },
118 | { "include": "#strIdentifiers" },
119 | {
120 | "match": "([a-zA-Z_]\\w*)\\s*,?",
121 | "captures": { "1": { "name": "storage.type.blink" } }
122 | }
123 | ]
124 | }
125 | ]
126 | },
127 | "autoClosingPairs": {
128 | "patterns": [
129 | { "include": "#brackets" },
130 | {
131 | "begin": "\"",
132 | "end": "\"",
133 | "name": "string.quoted.double.blink"
134 | },
135 | {
136 | "begin": "'",
137 | "end": "'",
138 | "name": "string.quoted.single.blink"
139 | }
140 | ]
141 | },
142 | "surroundingPairs": {
143 | "patterns": [
144 | { "include": "#brackets" },
145 | {
146 | "begin": "\"",
147 | "end": "\"",
148 | "name": "string.quoted.double.blink"
149 | },
150 | {
151 | "begin": "'",
152 | "end": "'",
153 | "name": "string.quoted.single.blink"
154 | }
155 | ]
156 | },
157 | "keywords": {
158 | "patterns": [
159 | {
160 | "match": "\\b(?:event|option|type|function|import|export|as|scope)\\b",
161 | "name": "keyword.control.blink"
162 | }
163 | ]
164 | },
165 | "typeKeywords": {
166 | "patterns": [
167 | {
168 | "match": "\\b(?:enum|map|struct|set)\\b",
169 | "name": "keyword.control.blink"
170 | }
171 | ]
172 | },
173 | "types": {
174 | "patterns": [
175 | {
176 | "match": "\\b(?:u8|u16|u32|i8|i16|i32|f16|f32|f64|boolean|string|buffer|unknown|Instance|Color3|vector|CFrame|BrickColor|DateTime|DateTimeMillis)\\b",
177 | "name": "storage.type.blink"
178 | }
179 | ]
180 | },
181 | "setting": {
182 | "patterns": [
183 | {
184 | "match": "\\b(?:Server|Client|Reliable|Unreliable|SingleSync|SingleAsync|ManySync|ManyAsync|Polling|Coroutine|Future|Promise|Pascal|Camel|Snake)\\b",
185 | "name": "entity.name.type.enum.blink"
186 | }
187 | ]
188 | },
189 | "operators": {
190 | "patterns": [
191 | {
192 | "match": "\\b(?::|true|false)\\b",
193 | "name": "constant.language.boolean.blink"
194 | }
195 | ]
196 | },
197 | "symbols": {
198 | "patterns": [
199 | {
200 | "match": "[=:]|\\.\\.+",
201 | "name": "keyword.operator.blink"
202 | },
203 | {
204 | "match": "\\?",
205 | "name": "keyword.operator.optional.blink"
206 | },
207 | {
208 | "match": ",",
209 | "name": "punctuation.separator.comma.blink"
210 | }
211 | ]
212 | },
213 | "tokenizer": {
214 | "patterns": [
215 | { "include": "#whitespace" },
216 | { "include": "#numbers" },
217 | { "include": "#delimiters" },
218 | { "include": "#strIdentifiers" },
219 | { "include": "#identifiersKeywords" }
220 | ]
221 | },
222 | "whitespace": {
223 | "patterns": [
224 | {
225 | "match": "[ \\t\\r\\n]+",
226 | "name": "text.whitespace.blink"
227 | },
228 | {
229 | "begin": "--\\[([=]*)\\[",
230 | "end": "$",
231 | "captures": { "1": { "name": "punctuation.definition.comment.blink" } },
232 | "name": "comment.block.documentation.blink"
233 | },
234 | {
235 | "match": "--.*$",
236 | "name": "comment.line.double-dash.blink"
237 | }
238 | ]
239 | },
240 | "numbers": {
241 | "patterns": [
242 | {
243 | "match": "\\d+?",
244 | "name": "constant.numeric.blink"
245 | }
246 | ]
247 | },
248 | "delimiters": {
249 | "patterns": [
250 | {
251 | "match": "[{}()\\[\\]]",
252 | "name": "punctuation.section.blink"
253 | }
254 | ]
255 | },
256 | "strIdentifiers": {
257 | "patterns": [
258 | {
259 | "match": "\"\\w+\"",
260 | "name": "string.quoted.double.blink"
261 | }
262 | ]
263 | },
264 | "identifiersKeywords": {
265 | "patterns": [
266 | { "include": "#symbols" },
267 | { "include": "#types" },
268 | { "include": "#keywords" },
269 | {
270 | "match": "([a-zA-Z_]\\w*):",
271 | "captures": { "1": { "name": "variable.other.readwrite.blink" } }
272 | },
273 | {
274 | "match": "[a-zA-Z_]\\w*",
275 | "name": "variable.other.blink"
276 | }
277 | ]
278 | }
279 | }
280 | }
--------------------------------------------------------------------------------
/docs/theme.config.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useRouter } from "next/router"
3 | import { DocsThemeConfig } from 'nextra-theme-docs'
4 |
5 | const config: DocsThemeConfig = {
6 | useNextSeoProps() {
7 | const { asPath } = useRouter()
8 | if (asPath !== "/") {
9 | return {
10 | titleTemplate: "%s – Blink",
11 | }
12 | }
13 | },
14 | logo: (
15 | <>
16 |
21 | >
22 | ),
23 | project: {
24 | link: 'https://github.com/1Axen/blink',
25 | },
26 | docsRepositoryBase: 'https://github.com/1Axen/blink',
27 | footer: {
28 | text: '© 2024 Blink',
29 | },
30 | head: (
31 | <>
32 |
33 |
34 | >
35 | )
36 | }
37 |
38 | export default config
39 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "incremental": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "preserve",
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "strictNullChecks": true
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts"
33 | ],
34 | "exclude": [
35 | "node_modules"
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/plugin/.darklua.json:
--------------------------------------------------------------------------------
1 | {
2 | "bundle": {
3 | "excludes": [
4 | "@lune/**"
5 | ],
6 | "modules_identifier": "__DARKLUA_BUNDLE_MODULES",
7 | "require_mode": {
8 | "name": "path",
9 | "sources": {
10 | "Base": "../src/Templates/Base.txt",
11 | "Client": "../src/Templates/Client.txt",
12 | "Server": "../src/Templates/Server.txt"
13 | }
14 | }
15 | },
16 | "generator": "readable",
17 | "rules": [
18 | "remove_types",
19 | "compute_expression",
20 | "remove_unused_if_branch",
21 | {
22 | "identifier": "BUNDLED",
23 | "rule": "inject_global_value",
24 | "value": true
25 | },
26 | {
27 | "identifier": "VERSION",
28 | "rule": "inject_global_value",
29 | "value": "0.17.3"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/plugin/build.bat:
--------------------------------------------------------------------------------
1 | @echo Off
2 |
3 | mkdir "./bundle"
4 | copy ".\src\Error.rbxmx" ".\bundle\Error.rbxmx"
5 | copy ".\src\Widget.rbxmx" ".\bundle\Widget.rbxmx"
6 | copy "..\build\.darklua.json" ".\.darklua.json"
7 |
8 | darklua process "./src/init.server.luau" "./bundle/init.server.lua"
9 | rojo build bundle.project.json --plugin "Blink.rbxmx"
10 |
11 | cd ..
12 | rojo sourcemap --output sourcemap.json --include-non-scripts
13 | cd plugin
--------------------------------------------------------------------------------
/plugin/bundle.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plugin",
3 | "tree": {
4 | "$path": "bundle"
5 | }
6 | }
--------------------------------------------------------------------------------
/plugin/src/Editor/Styling/AutoBracket.luau:
--------------------------------------------------------------------------------
1 | local Utility = require("../Utility")
2 |
3 | local Hook = {}
4 |
5 | local Brackets = {
6 | ["("] = ")",
7 | ["{"] = "}",
8 | ["["] = "]",
9 | ["<"] = ">",
10 | }
11 |
12 | local ClosingBrackets = {
13 | [")"] = true,
14 | ["}"] = true,
15 | ["]"] = true,
16 | [">"] = true,
17 | }
18 |
19 | local IgnoredCharacters = {
20 | ["\n"] = true,
21 | [""] = true,
22 | [")"] = true,
23 | ["}"] = true,
24 | ["]"] = true,
25 | [">"] = true,
26 | }
27 |
28 | local function OnGain(Text: string, Cursor: number): (string, number)
29 | local Next = string.sub(Text, Cursor, Cursor)
30 | local Previous = string.sub(Text, Cursor - 1, Cursor - 1)
31 |
32 | local ClosingBracket = Brackets[Previous]
33 | if ClosingBracket and IgnoredCharacters[Next] then
34 | Text = string.sub(Text, 1, Cursor - 1) .. ClosingBracket .. string.sub(Text, Cursor)
35 | return Text, Cursor
36 | end
37 |
38 | return Text, Cursor
39 | end
40 |
41 | local function OnRemove(Text: string, PreviousText: string, Cursor: number): (string, number)
42 | local Next = string.sub(Text, Cursor, Cursor)
43 | local Previous = string.sub(PreviousText, Cursor, Cursor)
44 | if Brackets[Previous] and ClosingBrackets[Next] then
45 | Text = string.sub(Text, 1, Cursor - 1) .. string.sub(Text, Cursor + 1)
46 | return Text, Cursor
47 | end
48 |
49 | return Text, Cursor
50 | end
51 |
52 | function Hook.OnSourceChanged(Text: string, PreviousText: string, Cursor: number, Gain: number): (string, number)
53 | if Gain >= 1 then
54 | return OnGain(Text, Cursor)
55 | elseif Gain <= -1 then
56 | return OnRemove(Text, PreviousText, Cursor)
57 | end
58 |
59 | return Text, Cursor
60 | end
61 |
62 | return Hook
--------------------------------------------------------------------------------
/plugin/src/Editor/Styling/AutoIndent.luau:
--------------------------------------------------------------------------------
1 | local Utility = require("../Utility")
2 |
3 | local Hook = {}
4 | local IndentKeywords = {
5 | "{\n"
6 | }
7 |
8 | local function ShouldAutoIndent(Text: string, Cursor: number): boolean
9 | for Index, Keyword in IndentKeywords do
10 | local Position = (Cursor - #Keyword)
11 | if Position <= 0 then
12 | continue
13 | end
14 |
15 | local Previous = string.sub(Text, Position, Cursor - 1)
16 | if Previous == Keyword then
17 | return true
18 | end
19 | end
20 |
21 | return false
22 | end
23 |
24 | local function GetLineIndentation(Line: string): number
25 | return #(string.match(Line, "^\t*") :: string)
26 | end
27 |
28 | function Hook.OnSourceChanged(Text: string, PreviousText: string, Cursor: number, Gain: number): (string, number)
29 | if Gain ~= 1 then
30 | return Text, Cursor
31 | end
32 |
33 | local CanIndent = false
34 | local AdditionalIndent = 0
35 | local Line, Lines = Utility.GetCurrentLine(Text, Cursor)
36 |
37 | local Current = Lines[Line]
38 | local Previous = Lines[Line - 1]
39 | local JustReached = (Previous and Current == "")
40 |
41 | if ShouldAutoIndent(Text, Cursor) then
42 | CanIndent = true
43 | AdditionalIndent = 1
44 | elseif JustReached then
45 | if GetLineIndentation(Previous) > 0 then
46 | CanIndent = true
47 | end
48 | end
49 |
50 | if not CanIndent then
51 | return Text, Cursor
52 | end
53 |
54 | --> Update text and cursor
55 | local NextCharacter = string.sub(Text, Cursor, Cursor)
56 | if string.gsub(NextCharacter, "%c", "") == "" then
57 | NextCharacter = nil
58 | end
59 |
60 | local Indentation = GetLineIndentation(Previous) + AdditionalIndent
61 | Text = string.sub(Text, 1, Cursor - 1) .. string.rep("\t", Indentation) .. (NextCharacter and `\n{string.rep("\t", Indentation - 1)}` or "") .. string.sub(Text, Cursor)
62 |
63 | return Text, Cursor + Indentation
64 | end
65 |
66 | return Hook
--------------------------------------------------------------------------------
/plugin/src/Editor/Utility.luau:
--------------------------------------------------------------------------------
1 | local Utility = {}
2 |
3 | export type Range = {
4 | Text: string,
5 | Index: number,
6 | Start: number,
7 | End: number
8 | }
9 |
10 | local DEFAULT_RANGE = {
11 | Text = "",
12 | Index = 0,
13 | Start = 0,
14 | End = 0
15 | }
16 |
17 | local function Split(String: string): {Range}
18 | local Cursor = 1
19 | local Ranges: {Range} = {}
20 |
21 | while Cursor <= #String do
22 | --> Ignore whitespaces
23 | local Start, End = string.find(String, "^[%s%.]+", Cursor)
24 | if Start and End then
25 | Cursor = (End + 1)
26 | continue
27 | end
28 |
29 | --> Match words
30 | Start, End = string.find(String, "^[%w%p]+", Cursor)
31 |
32 | if Start then
33 | local Text = string.sub(String, Start, End)
34 | table.insert(Ranges, {
35 | Text = Text,
36 | Index = #Ranges + 1,
37 | Start = Start,
38 | End = End
39 | })
40 |
41 | Cursor = (End + 1)
42 | continue
43 | end
44 |
45 | break
46 | end
47 |
48 | return Ranges
49 | end
50 |
51 | function Utility.GetCurrentLine(Source: string, Cursor: number): (number, {string})
52 | local Line = 0
53 | local Position = 0
54 | local Slices = string.split(Source, "\n")
55 |
56 | for Index, Slice in Slices do
57 | Position += (#Slice + 1)
58 | if Cursor <= Position then
59 | Line = Index
60 | break
61 | end
62 | end
63 |
64 | return Line, Slices
65 | end
66 |
67 | function Utility.GetLineRangeAtPosition(Source: string, Cursor: number): Range
68 | local Range: Range;
69 | local Position = 0
70 | local Slices = string.split(Source, "\n")
71 |
72 | for Index, Slice in Slices do
73 | local Start = Position
74 | Position += (#Slice + 1)
75 | if Cursor <= Position then
76 | Range = {
77 | Text = Slice,
78 | Index = 1,
79 | Start = (Start + 1),
80 | End = (Position - 1)
81 | }
82 |
83 | break
84 | end
85 | end
86 |
87 | return Range
88 | end
89 |
90 | function Utility.GetWordRangeAtPosition(Source: string, Position: number): Range
91 | local Ranges = Split(Source)
92 |
93 | for _, Range in Ranges do
94 | if Range.Start <= Position and Range.End >= Position then
95 | return Range
96 | end
97 | end
98 |
99 | return DEFAULT_RANGE
100 | end
101 |
102 | function Utility.GetWordRangeBeforePosition(Source: string, Position: number): Range
103 | local Ranges = Split(Source)
104 |
105 | for Index, Range in Ranges do
106 | if Range.Start >= Position then
107 | return Ranges[Index - 1] or DEFAULT_RANGE
108 | end
109 | end
110 |
111 | return DEFAULT_RANGE
112 | end
113 |
114 | return Utility
--------------------------------------------------------------------------------
/plugin/src/Error.rbxmx:
--------------------------------------------------------------------------------
1 |
2 | true
3 | null
4 | nil
5 | -
6 |
7 | false
8 |
9 | 0
10 | 0
11 |
12 |
13 | true
14 | 0
15 |
16 | 0.156862751
17 | 0.156862751
18 | 0.156862751
19 |
20 | 0
21 |
22 | 0
23 | 0
24 | 0
25 |
26 | 0
27 | 0
28 | 0
29 | false
30 | false
31 | false
32 | true
33 | 0
34 | Error
35 |
[null]
36 | [null]
37 | [null]
38 | [null]
39 |
40 | 0
41 | 0
42 | 0
43 | 0
44 |
45 | [null]
46 | 0
47 | false
48 | 0
49 | 0
50 | 0
51 | 0
52 | false
53 | [null]
54 | 0
55 |
56 | 1
57 | 0
58 | 1
59 | 0
60 |
61 | 0
62 | -1
63 | 0
64 |
65 | false
66 | 3
67 |
68 | -
69 |
70 | false
71 |
72 | 0
73 | 0
74 |
75 |
76 | true
77 | 0
78 |
79 | 1
80 | 1
81 | 1
82 |
83 | 1
84 |
85 | 0
86 | 0
87 | 0
88 |
89 | 0
90 | 0
91 | 0
92 | false
93 | false
94 | false
95 |
96 | rbxassetid://16658246179
97 | 400
98 |
99 |
100 | true
101 | 0
102 | 1
103 |
104 |
105 | -1
106 | Text
107 |
[null]
108 | [null]
109 | [null]
110 | [null]
111 |
112 |
113 | 0
114 | 0
115 | 0
116 | 0
117 |
118 | true
119 | [null]
120 | 0
121 | false
122 | 0
123 | 0
124 | 0
125 | 0
126 | false
127 | [null]
128 | 0
129 |
130 | 1
131 | 0
132 | 1
133 | 0
134 |
135 | 0
136 | -1
137 |
138 | Label
139 |
140 | 1
141 | 1
142 | 1
143 |
144 | 0
145 | false
146 | 16
147 |
148 | 0
149 | 0
150 | 0
151 |
152 | 1
153 | 0
154 | 0
155 | false
156 | 0
157 | 0
158 | true
159 | 1
160 |
161 |
162 | -
163 |
164 |
165 | 0
166 | false
167 | UIPadding
168 |
169 |
0
170 | 8
171 |
172 |
173 | 0
174 | 8
175 |
176 |
177 | 0
178 | 8
179 |
180 |
181 | 0
182 | 8
183 |
184 | -1
185 |
186 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/plugin/src/Profiler.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | ---- Services ----
4 |
5 | ---- Imports ----
6 |
7 | ---- Settings ----
8 |
9 | ---- Constants ----
10 |
11 | local Profiler = {}
12 |
13 | --> Interface Instances
14 | local Container: typeof(script.Parent.Widget) = script.Widget
15 | local DebugContainer = Container.Debug
16 | local MetricTemplate = DebugContainer.Metric:Clone()
17 |
18 | type Profile = {
19 | Label: string,
20 | Metric: typeof(MetricTemplate),
21 |
22 | Tick: number,
23 | Time: number,
24 |
25 | Previous: Profile?,
26 | Children: {Profile},
27 | }
28 |
29 | ---- Variables ----
30 |
31 | local ActiveProfile: Profile?
32 |
33 | ---- Private Functions ----
34 |
35 | local function isActive()
36 | return DebugContainer.Visible
37 | end
38 |
39 | local function formatPercentage(Percentage: number): string
40 | return `{math.floor(Percentage * 100)} %`
41 | end
42 |
43 | local function formatMilliseconds(Seconds: number): string
44 | return string.format("%.2f ms", Seconds * 1000)
45 | end
46 |
47 | local function updatePercentages(Time: number, Children: {Profile})
48 | for _, Profile in Children do
49 | Profile.Metric.Percentage.Text = formatPercentage(Profile.Time / Time)
50 | updatePercentages(Time, Profile.Children)
51 | end
52 | end
53 |
54 | ---- Public Functions ----
55 |
56 | function Profiler.profileBegin(Label: string)
57 | if not isActive() then
58 | return
59 | end
60 |
61 | local Metric = DebugContainer:FindFirstChild(Label)
62 | if not Metric then
63 | local New = MetricTemplate:Clone()
64 | New.Name = Label
65 | New.Time.Text = "? ms"
66 | New.Label.Text = string.upper(Label)
67 | New.Parent = DebugContainer
68 | Metric = New
69 | end
70 |
71 | local Profile: Profile = {
72 | Label = Label,
73 | Metric = Metric,
74 |
75 | Tick = os.clock(),
76 | Time = 0,
77 |
78 | Children = {},
79 | Previous = ActiveProfile,
80 | }
81 |
82 | if ActiveProfile then
83 | table.insert(ActiveProfile.Children, Profile)
84 | end
85 |
86 | ActiveProfile = Profile
87 | end
88 |
89 | function Profiler.profileDiscard()
90 | if not isActive() then
91 | return
92 | end
93 |
94 | assert(ActiveProfile ~= nil, "No active profile.")
95 | ActiveProfile = ActiveProfile.Previous
96 | end
97 |
98 | function Profiler.profileEnd()
99 | if not isActive() then
100 | return
101 | end
102 |
103 | assert(ActiveProfile ~= nil, "No active profile.")
104 | local Elapsed = (os.clock() - ActiveProfile.Tick)
105 |
106 | ActiveProfile.Time = Elapsed
107 | ActiveProfile.Metric.Time.Text = formatMilliseconds(Elapsed)
108 |
109 | if not ActiveProfile.Previous then
110 | updatePercentages(Elapsed, ActiveProfile.Children)
111 | end
112 |
113 | ActiveProfile = ActiveProfile.Previous
114 | end
115 |
116 | function Profiler.toggle()
117 | DebugContainer.Visible = not DebugContainer.Visible
118 | end
119 |
120 | ---- Initialization ----
121 |
122 | function Profiler.Initialize()
123 | DebugContainer.Metric:Destroy()
124 | end
125 |
126 | ---- Connections ----
127 |
128 | return Profiler
129 |
--------------------------------------------------------------------------------
/plugin/src/State.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | -- ******************************* --
4 | -- AX3NX / AXEN --
5 | -- ******************************* --
6 |
7 | ---- Services ----
8 |
9 | ---- Imports ----
10 |
11 | ---- Settings ----
12 |
13 | ---- Constants ----
14 |
15 | local State = {}
16 | State.__index = State
17 |
18 | export type Class = typeof(setmetatable({} :: {
19 | Value: T,
20 | Observers: {[(T) -> ()]: boolean}
21 | }, State))
22 |
23 | ---- Variables ----
24 |
25 | ---- Private Functions ----
26 |
27 | ---- Public Functions ----
28 |
29 | function State.new(Value: T): Class
30 | return setmetatable({
31 | Value = Value,
32 | Observers = {}
33 | }, State)
34 | end
35 |
36 | function State.Get(self: Class): T
37 | return self.Value
38 | end
39 |
40 | function State.Set(self: Class, Value: T)
41 | if
42 | self.Value ~= Value
43 | or type(Value) == "table"
44 | then
45 | self.Value = Value
46 | self:_updateObservers()
47 | end
48 | end
49 |
50 | function State.OnChange(self: Class, Observer: (T) -> ()): () -> ()
51 | self.Observers[Observer] = true
52 | task.defer(function()
53 | self:_updateObservers()
54 | end)
55 |
56 | return function()
57 | self.Observers[Observer] = nil
58 | end
59 | end
60 |
61 | function State._updateObservers(self: Class)
62 | for Observer in self.Observers do
63 | task.spawn(Observer, self.Value)
64 | end
65 | end
66 |
67 | ---- Initialization ----
68 |
69 | ---- Connections ----
70 |
71 | return State
72 |
--------------------------------------------------------------------------------
/plugin/src/Table.luau:
--------------------------------------------------------------------------------
1 | local Table = {}
2 |
3 | function Table.MergeArrays(a: {any}, b: {any}): {any}
4 | local Array = table.create(#a + #b)
5 |
6 | for _, Element in a do
7 | table.insert(Array, Element)
8 | end
9 |
10 | for _, Element in b do
11 | table.insert(Array, Element)
12 | end
13 |
14 | return Array
15 | end
16 |
17 | function Table.MergeDictionaries(a: {[any]: any}, b: {[any]: any}): {[any]: any}
18 | local Dictionary = table.clone(a)
19 | for Key, Value in b do
20 | if Dictionary[Key] then
21 | warn(`Key "{Key}" already exists in the first dictionary.`)
22 | continue
23 | end
24 |
25 | Dictionary[Key] = Value
26 | end
27 | return Dictionary
28 | end
29 |
30 | function Table.GetDictionaryKeys(Dictionary: {[any]: any}): {any}
31 | local Keys = {}
32 | for Key in Dictionary do
33 | table.insert(Keys, Key)
34 | end
35 | return Keys
36 | end
37 |
38 | return Table
--------------------------------------------------------------------------------
/rokit.toml:
--------------------------------------------------------------------------------
1 | # This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
2 | # For more information, see https://github.com/rojo-rbx/rokit
3 |
4 | # New tools can be added by running `rokit add ` in a terminal.
5 |
6 | [tools]
7 | selene = "Kampfkarren/selene@0.26.1"
8 | lune = "filiptibell/lune@0.8.9"
9 | darklua = "seaofvoices/darklua@0.14.0"
10 | rojo = "rojo-rbx/rojo@7.4.1"
11 | run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
12 |
--------------------------------------------------------------------------------
/src/CLI/Utility/Compile.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | ---- Imports ----
4 |
5 | local fs = require("@lune/fs")
6 | local stdio = require("@lune/stdio")
7 |
8 | local Error = require("../../Modules/Error")
9 | local Parser = require("../../Parser")
10 | local Generator = require("../../Generator/init.luau")
11 | local PathParser = require("../../Modules/Path")
12 | local GetDefinitionFilePath = require("./GetDefinitionFilePath")
13 |
14 | ---- Settings ----
15 |
16 | export type CompileOptions = {
17 | Debug: boolean,
18 | Silent: boolean,
19 | Compact: boolean,
20 | YesToEverything: boolean,
21 | OutputAbstractSyntaxTree: boolean,
22 | }
23 |
24 | ---- Constants ----
25 |
26 | local Utility = {}
27 |
28 | ---- Variables ----
29 |
30 | ---- Private Functions ----
31 |
32 | local function Message(Color: stdio.Color, Options: CompileOptions, ...)
33 | if Options.Silent then
34 | return
35 | end
36 |
37 | stdio.write(stdio.color(Color))
38 | print(...)
39 | stdio.write(stdio.color("reset"))
40 | end
41 |
42 | local function FormatTime(Time: number): string
43 | if Time < 1E-6 then
44 | return `{Time * 1E+9} ns`
45 | elseif Time < 0.001 then
46 | return `{Time * 1E+6} μs`
47 | elseif Time < 1 then
48 | return `{Time * 1000} ms`
49 | else
50 | return `{Time} seconds`
51 | end
52 | end
53 |
54 | local function CreateDirectoryIfNotFound(Path: string, Options: CompileOptions)
55 | local Directory = PathParser.Directory(Path) or "./"
56 | if fs.isDir(Directory) then
57 | return
58 | end
59 |
60 | local Result = Options.YesToEverything
61 | or stdio.prompt("confirm", `Directory {Directory} doesn't exist, create directory?`)
62 |
63 | if Result then
64 | fs.writeDir(Directory)
65 | return
66 | end
67 |
68 | error("User refused prompt to create directory")
69 | end
70 |
71 | ---- Public Functions ----
72 |
73 | function Utility.Compile(Path: string, Options: CompileOptions)
74 | local FilePath = GetDefinitionFilePath(Path)
75 | Error.SetCompact(Options.Compact)
76 |
77 | Message("cyan", Options, `Reading source from {FilePath}...`)
78 | local Source = fs.readFile(FilePath)
79 |
80 | Message("blue", Options, "Parsing source into AST...")
81 | local FileDirectory = PathParser.Directory(FilePath) or "./"
82 | local SourceParser = Parser.new(FileDirectory, PathParser.NameWithExtension(FilePath))
83 |
84 | local ParseStart = os.clock()
85 | local AbstractSyntaxTree = SourceParser:Parse(Source)
86 | local ParseTime = (os.clock() - ParseStart)
87 |
88 | if Options.OutputAbstractSyntaxTree then
89 | print(AbstractSyntaxTree.Value.Options)
90 | print(AbstractSyntaxTree.Value.Declarations)
91 | return
92 | end
93 |
94 | local FileOptions = AbstractSyntaxTree.Value.Options
95 | local ClientOutputPath = FileOptions.ClientOutput
96 | local ServerOutputPath = FileOptions.ServerOutput
97 |
98 | assert(ClientOutputPath, "A client output path must be defined.")
99 | assert(ServerOutputPath, "A server output path must be defined.")
100 |
101 | local ClientTypescriptPath;
102 | local ServerTypescriptPath;
103 |
104 | do
105 | local Directory, Filename = PathParser.Components(ClientOutputPath)
106 | ClientOutputPath = `{Directory}/{Filename}.luau`
107 | ClientTypescriptPath = `{FileDirectory}{Directory}/{Filename}.d.ts`
108 | end
109 |
110 | do
111 | local Directory, Filename = PathParser.Components(ServerOutputPath)
112 | ServerOutputPath = `{Directory}/{Filename}.luau`
113 | ServerTypescriptPath = `{FileDirectory}{Directory}/{Filename}.d.ts`
114 | end
115 |
116 | local TypesOutput: string;
117 | local ClientOutput = `{FileDirectory}{ClientOutputPath}`
118 | local ServerOutput = `{FileDirectory}{ServerOutputPath}`
119 |
120 | CreateDirectoryIfNotFound(ClientOutput, Options)
121 | CreateDirectoryIfNotFound(ServerOutput, Options)
122 |
123 | Message("blue", Options, "Generating output files...")
124 | local FilesStart = os.clock()
125 |
126 | local ServerGenerateStart = os.clock()
127 | local ServerSource = Generator.Generate("Server", AbstractSyntaxTree)
128 | local ServerGenerateTime = (os.clock() - ServerGenerateStart)
129 |
130 | local ClientGenerateStart = os.clock()
131 | local ClientSource = Generator.Generate("Client", AbstractSyntaxTree)
132 | local ClientGenerateTime = (os.clock() - ClientGenerateStart)
133 |
134 | local TypesSource: string?;
135 | if FileOptions.TypesOutput then
136 | TypesOutput = `{FileDirectory}{FileOptions.TypesOutput}`
137 | TypesSource = Generator.GenerateShared(AbstractSyntaxTree)
138 | end
139 |
140 | local ClientTypescriptSource: string?
141 | local ServerTypescriptSource: string?
142 | if FileOptions.Typescript then
143 | ClientTypescriptSource = Generator.GenerateTypescript("Client", AbstractSyntaxTree)
144 | ServerTypescriptSource = Generator.GenerateTypescript("Server", AbstractSyntaxTree)
145 | end
146 |
147 | local FilesTime = (os.clock() - FilesStart)
148 | fs.writeFile(ServerOutput, ServerSource)
149 | fs.writeFile(ClientOutput, ClientSource)
150 |
151 | if TypesSource and TypesOutput then
152 | CreateDirectoryIfNotFound(TypesOutput, Options)
153 | fs.writeFile(TypesOutput, TypesSource)
154 | end
155 |
156 | if ClientTypescriptSource and ServerTypescriptSource then
157 | fs.writeFile(ClientTypescriptPath, ClientTypescriptSource)
158 | fs.writeFile(ServerTypescriptPath, ServerTypescriptSource)
159 | end
160 |
161 | Message("green", Options, "Network files generated!")
162 |
163 | if Options.Debug and not Options.Silent then
164 | print(`[DEBUG]: Parsed source in {FormatTime(ParseTime)}.`)
165 | print(`[DEBUG]: Generated server luau in {FormatTime(ServerGenerateTime)}.`)
166 | print(`[DEBUG]: Generated client luau in {FormatTime(ClientGenerateTime)}.`)
167 | print(`[DEBUG]: Generated files in {FormatTime(FilesTime)}.`)
168 | print(`[DEBUG]: Completed everything in {FormatTime(ParseTime + FilesTime)}.`)
169 | end
170 | end
171 |
172 | ---- Initialization ----
173 |
174 | ---- Connections ----
175 |
176 | return Utility
--------------------------------------------------------------------------------
/src/CLI/Utility/GetDefinitionFilePath.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | ---- Imports ----
4 |
5 | local fs = require("@lune/fs")
6 | local PathParser = require("../../Modules/Path")
7 |
8 | ---- Settings ----
9 |
10 | local EXTENSIONS = {"", ".txt", ".blink"}
11 |
12 | return function(Path: string): string
13 | local Directory, Filename = PathParser.Components(Path)
14 | Directory = Directory or "./"
15 | assert(Filename, `Unable to parse filename from {Path}, path may be malformed!`)
16 |
17 | for _, Extension in EXTENSIONS do
18 | local Temporary = `{Directory}{Filename}{Extension}`
19 | if fs.isFile(Temporary) then
20 | return Temporary
21 | end
22 | end
23 |
24 | return Path
25 | end
26 |
--------------------------------------------------------------------------------
/src/CLI/Utility/Watch.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | ---- Imports ----
4 |
5 | local fs = require("@lune/fs")
6 | local task = require("@lune/task")
7 | local stdio = require("@lune/stdio")
8 | local process = require("@lune/process")
9 | local dateTime = require("@lune/datetime")
10 |
11 | local Compile = require("./Compile")
12 | local PathParser = require("../../Modules/Path")
13 | local GetDefinitionFilePath = require("./GetDefinitionFilePath")
14 |
15 | ---- Settings ----
16 |
17 | type DateTime = dateTime.DateTime
18 |
19 | local IMPORT_PATTERN = `import "([%w%/%.-_]+)"`
20 | local WATCH_OPTIONS: Compile.CompileOptions = {
21 | Debug = false,
22 | Silent = true,
23 | Compact = false,
24 | YesToEverything = true,
25 | OutputAbstractSyntaxTree = false
26 | }
27 |
28 | ---- Constants ----
29 |
30 | local Utility = {}
31 |
32 | ---- Variables ----
33 |
34 | ---- Private Functions ----
35 |
36 | local function Color(Color: stdio.Color, Text: string): string
37 | return `{stdio.color(Color)}{Text}{stdio.color("reset")}`
38 | end
39 |
40 | local function BuildImportsArray(Entry: string, PreviousArray: {string}?): {string}
41 | local Array = PreviousArray or {}
42 | table.insert(Array, Entry)
43 |
44 | local Source = fs.readFile(Entry)
45 | local Directory = PathParser.Directory(Entry)
46 | local Iterator = string.gmatch(Source, IMPORT_PATTERN)
47 |
48 | while (true) do
49 | local Path = Iterator()
50 | if not Path then
51 | break
52 | end
53 |
54 | Path = `{Directory}{Path}`
55 | Path = GetDefinitionFilePath(Path)
56 |
57 | if not fs.isFile(Path) then
58 | error(`No file to import at "{Path}"`)
59 | end
60 |
61 | --> Prevent repeats
62 | if table.find(Array, Path) then
63 | continue
64 | end
65 |
66 | BuildImportsArray(Path, Array)
67 | end
68 |
69 | return Array
70 | end
71 |
72 | ---- Public Functions ----
73 |
74 | function Utility.Watch(Path: string)
75 | local FilePath = GetDefinitionFilePath(Path)
76 | local Imports: {string};
77 |
78 | _G.WATCH_THREAD = true
79 |
80 | local function Traverse()
81 | --> Resolve imports
82 | local OldImports = Imports and #Imports or 0
83 | Imports = BuildImportsArray(FilePath)
84 |
85 | if OldImports ~= #Imports then
86 | print(`Blink is watching for changes:\n\tEntry: {Color("yellow", FilePath)}\n\tImports: {Color("yellow", tostring(#Imports - 1))}`)
87 | end
88 |
89 | return true
90 | end
91 |
92 | local function Recompile()
93 | Traverse()
94 | Compile.Compile(Path, WATCH_OPTIONS)
95 | end
96 |
97 | --> Initial traversal
98 | local InitialSuccess, Why = pcall(Traverse)
99 | if not InitialSuccess then
100 | warn(`There was an error while trying to start the watcher thread:\n{Why}`)
101 | return
102 | end
103 |
104 | --> Watch loop
105 | local Timestamps: {[string]: number} = {}
106 |
107 | while true do
108 | local FileChanged = false
109 | for _, File in Imports do
110 | local Metadata = fs.metadata(File)
111 |
112 | --> Make sure file still exists
113 | if not Metadata.exists then
114 | continue
115 | end
116 |
117 | local ModifiedAt = Metadata.modifiedAt and Metadata.modifiedAt.unixTimestampMillis or 0
118 | local LastModifiedA = Timestamps[File]
119 | Timestamps[File] = ModifiedAt
120 |
121 | if ModifiedAt ~= LastModifiedA then
122 | FileChanged = true
123 | end
124 | end
125 |
126 | if FileChanged then
127 | pcall(Recompile)
128 | end
129 |
130 | task.wait(1)
131 | end
132 | end
133 |
134 | ---- Initialization ----
135 |
136 | ---- Connections ----
137 |
138 | return Utility
--------------------------------------------------------------------------------
/src/CLI/init.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | --!optimize 2
3 |
4 | ---- Imports ----
5 |
6 | local stdio = require("@lune/stdio")
7 | local process = require("@lune/process")
8 |
9 | local Watch = require("./Utility/Watch.luau")
10 | local Compile = require("./Utility/Compile.luau")
11 |
12 | ---- Settings ----
13 |
14 | type Argument = {
15 | Hidden: true?,
16 | Aliases: {string},
17 | Description: string
18 | }
19 |
20 | local ARGUMENTS: {[string]: Argument} = {
21 | Help = {
22 | Aliases = {"-h", "--help"},
23 | Description = "Print help information"
24 | },
25 | Version = {
26 | Aliases = {"-v", "--version"},
27 | Description = "Print version information"
28 | },
29 | Watch = {
30 | Aliases = {"-w", "--watch"},
31 | Description = "Watch [CONFIG] for changes, automatically recompile upon one occuring"
32 | },
33 | Silent = {
34 | Aliases = {"-q", "--quiet"},
35 | Description = "Silence program output"
36 | },
37 | Compact = {
38 | Aliases = {"-c", "--compact"},
39 | Description = "Compacts error output, the full message is still printed out after a compacted version."
40 | },
41 | YesToAll = {
42 | Aliases = {"-y", "--yes"},
43 | Description = "Accept all prompts"
44 | },
45 | OutputAst = {
46 | Hidden = true,
47 | Aliases = {"--ast"},
48 | Description = "Output the AST of [CONFIG]"
49 | },
50 | Statistics = {
51 | Hidden = true,
52 | Aliases = {"-S", "--stats"},
53 | Description = "Output compilation time statistics"
54 | }
55 | }
56 |
57 | ---- Functions -----
58 |
59 | local function Style(Text: string, Style: stdio.Style): string
60 | return `{stdio.style(Style)}{Text}{stdio.style("reset")}`
61 | end
62 |
63 | local function Color(Text: string, Color: stdio.Color): string
64 | return `{stdio.color(Color)}{Text}{stdio.color("reset")}`
65 | end
66 |
67 | local function PrintHelpMessage()
68 | print(Color(Style("USAGE:", "bold"), "yellow"))
69 | print("\tblink.exe [CONFIG] [OPTIONS]")
70 | print(Color(Style("OPTIONS:", "bold"), "yellow"))
71 |
72 | local Sorted: {Argument} = {}
73 | for _, Argument in ARGUMENTS do
74 | if not Argument.Hidden then
75 | table.insert(Sorted, Argument)
76 | end
77 | end
78 |
79 | table.sort(Sorted, function(a, b)
80 | return a.Aliases[1] < b.Aliases[1]
81 | end)
82 |
83 | for _, Argument in Sorted do
84 | print(`\t{stdio.color("green")}{table.concat(Argument.Aliases, ", ")}{stdio.color("reset")}\t{Argument.Description}`)
85 | end
86 | end
87 |
88 | ---- Main ----
89 |
90 | print(`{Color("Blink", "green")} {_G.VERSION or "DEBUG"}`)
91 |
92 | local RawArguments = process.args
93 | if #RawArguments < 1 then
94 | PrintHelpMessage()
95 | return
96 | end
97 |
98 | local Path = RawArguments[1]
99 |
100 | --> Parse optional arguments
101 | local Arguments = {}
102 | for Key, Argument in ARGUMENTS do
103 | local IsSupplied = false
104 | for _, Alias in Argument.Aliases do
105 | if table.find(RawArguments, Alias) ~= nil then
106 | IsSupplied = true
107 | break
108 | end
109 | end
110 |
111 | Arguments[Key] = IsSupplied
112 | end
113 |
114 | if Arguments.Help then
115 | PrintHelpMessage()
116 | return
117 | end
118 |
119 | if Arguments.Version then
120 | return
121 | end
122 |
123 | if not Arguments.Watch then
124 | local CompileOptions: Compile.CompileOptions = {
125 | Debug = (_G.BUNDLED == nil) or Arguments.Statistics,
126 | Silent = Arguments.Silent,
127 | Compact = Arguments.Compact,
128 | YesToEverything = Arguments.YesToAll,
129 | OutputAbstractSyntaxTree = Arguments.OutputAst
130 | }
131 |
132 | Compile.Compile(Path, CompileOptions)
133 | else
134 | Watch.Watch(Path)
135 | end
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/src/Generator/Blocks.luau:
--------------------------------------------------------------------------------
1 | type BranchType = "Conditional" | "Default"
2 | type EqualityOperator = "Not" | "Equals" | "Greater" | "Less" | "GreaterOrEquals" | "LessOrEquals"
3 |
4 | type Method = "Read" | "Allocate" | "None"
5 | type Operation = {
6 | Method: Method,
7 | Bytes: number,
8 | Counts: number,
9 | Variable: string?
10 | }
11 |
12 | local DEFAULT_UNIQUE = "BLOCK_START"
13 | local ARRAY_UNIQUE = "ARRAY_START"
14 |
15 | local Operators = {
16 | Not = "~=",
17 | Equals = "==",
18 | Greater = ">",
19 | Less = "<",
20 | GreaterOrEquals = ">=",
21 | LessOrEquals = "<=",
22 | }
23 |
24 | local Block = {}
25 | Block.__index = Block
26 |
27 | export type Block = typeof(setmetatable({} :: {
28 | Unique: string,
29 | Parent: Block?,
30 | Indent: number,
31 | Cursor: number,
32 | Unsafe: boolean,
33 | Content: {string},
34 | Operation: Operation,
35 | Variables: {[string]: boolean},
36 | OperationOffset: number,
37 | }, Block))
38 |
39 | function Block.new(Parent: Block?, Unique: string?): Block
40 | local Indent = (Parent and Parent.Indent + 1 or 1)
41 | return setmetatable({
42 | Unique = Unique or DEFAULT_UNIQUE,
43 | Parent = Parent,
44 | Indent = Indent,
45 | Cursor = 1,
46 | Unsafe = false,
47 | Content = table.create(64),
48 | Operation = {
49 | Method = "None",
50 | Bytes = 0,
51 | Counts = 0
52 | },
53 | Variables = {},
54 | OperationOffset = 0,
55 | }, Block)
56 | end
57 |
58 | function Block.DisableOperationOptimisations(self: Block): Block
59 | self.Unsafe = true
60 | return self
61 | end
62 |
63 | function Block._operation(self: Block, Method: Method, Bytes: number): string
64 | local Operation = self.Operation
65 | if Operation.Method ~= "None" and Operation.Method ~= Method then
66 | error(`Block can't both Allocate and Read`)
67 | end
68 |
69 | if self.Unsafe then
70 | local Counts = Operation.Counts
71 | local Variable = `OFFSET_{Counts}`
72 |
73 | Operation.Counts += 1
74 | self:Line(`local {Variable} = {Method}({Bytes})`)
75 |
76 | return Variable
77 | end
78 |
79 | local Offset = Operation.Bytes
80 | Operation.Bytes += Bytes
81 | Operation.Method = Method
82 |
83 | if self.Unique ~= DEFAULT_UNIQUE then
84 | self:Comment(`{Method} {Bytes}`)
85 | local OperationOffset = `OPERATION_OFFSET_{self.OperationOffset}`
86 | self.OperationOffset += 1
87 | self:Line(`local {OperationOffset} = {self.Unique}`)
88 | self:Line(`{self.Unique} += {Bytes}`)
89 | return OperationOffset
90 | end
91 |
92 | return `{self.Unique} + {Offset}`
93 | end
94 |
95 | function Block.Read(self: Block, Bytes: number): string
96 | return self:_operation("Read", Bytes)
97 | end
98 |
99 | function Block.Allocate(self: Block, Bytes: number): string
100 | return self:_operation("Allocate", Bytes)
101 | end
102 |
103 | function Block._lineFront(self: Block, Text: string, Indent: number): Block
104 | table.insert(self.Content, self.Cursor, `{string.rep("\t", Indent or self.Indent)}{Text}\n`)
105 | return self
106 | end
107 |
108 | function Block._appendOperations(self: Block): Block
109 | local Operation = self.Operation
110 | local Variable = Operation.Variable
111 | if Operation.Method == "None" then
112 | return self
113 | end
114 |
115 | local Append = `local {self.Unique} = {Operation.Method}({Operation.Bytes}{Variable and ` * {Variable}` or ""})`
116 |
117 | self:_lineFront(Append, self.Unique ~= DEFAULT_UNIQUE and self.Indent - 1 or self.Indent)
118 | self:_lineFront(`-- {Operation.Method} BLOCK: {Operation.Bytes} bytes`)
119 |
120 | return self
121 | end
122 |
123 | function Block.Declare(self: Block, Variable: string, Initialize: boolean?): (string)
124 | local Block = self
125 | local Declared = false
126 | local IsFieldAccess = (string.find(Variable, "[%.%[]", 1) ~= nil)
127 |
128 | --> Ignore field acceses
129 | if IsFieldAccess then
130 | return Variable
131 | end
132 |
133 | --> Search for variable declaration in block hierarchy
134 | while (Block) do
135 | if Block.Variables[Variable] then
136 | Declared = true
137 | break
138 | end
139 |
140 | Block = Block.Parent
141 | end
142 |
143 | if Declared then
144 | return Variable
145 | end
146 |
147 | --> Declare variable within current block
148 | self.Variables[Variable] = true
149 |
150 | if Initialize then
151 | self:Line(`local {Variable}`)
152 | end
153 |
154 | return `local {Variable}`
155 | end
156 |
157 | function Block.EmptyDeclare(self: Block, Variable: string)
158 | self.Variables[Variable] = true
159 | end
160 |
161 | function Block.Advance(self: Block, Offset: number): Block
162 | self.Cursor += Offset
163 | return self
164 | end
165 |
166 | function Block.Line(self: Block, Text: string, Indent: number?): Block
167 | table.insert(self.Content, `{string.rep("\t", Indent or self.Indent)}{Text}\n`)
168 | return self
169 | end
170 |
171 | function Block.Character(self: Block, Character: string): Block
172 | table.insert(self.Content, Character)
173 | return self
174 | end
175 |
176 | function Block.Comment(self: Block, Content: string): Block
177 | self:Line(`-- {Content}`)
178 | return self
179 | end
180 |
181 | function Block.Lines(self: Block, Lines: {string}, Indent: number?): Block
182 | local Indent = Indent or 0
183 |
184 | --> FAST PATH
185 | if Indent == 0 then
186 | table.move(Lines, 1, #Lines, #self.Content + 1, self.Content)
187 | return self
188 | end
189 |
190 | local Indentation = string.rep("\t", Indent)
191 |
192 | for Index, Line in Lines do
193 | table.insert(self.Content, `{Indentation}{Line}`)
194 | end
195 |
196 | return self
197 | end
198 |
199 | function Block.Multiline(self: Block, Content: string, Indent: number?): Block
200 | local Lines = string.split(Content, "\n")
201 | for Index, Line in Lines do
202 | table.insert(self.Content, `{string.rep("\t", Indent or 0)}{Line}\n`)
203 | end
204 |
205 | return self
206 | end
207 |
208 | function Block.Loop(self: Block, Counter: string, Length: string): Block
209 | local Loop = Block.new(self, `{ARRAY_UNIQUE}_{self.Indent}`)
210 | Loop:Line(`for {Counter}, {Length} do`, Loop.Indent - 1)
211 | Loop.Operation.Variable = Length
212 | return Loop
213 | end
214 |
215 | function Block.While(self: Block, Condition: string): Block
216 | self:Line(`while ({Condition}) do`)
217 | return Block.new(self)
218 | end
219 |
220 | function Block.Iterator(self: Block, Key: string, Value: string, Iterator: string): Block
221 | self:Line(`for {Key}, {Value} in {Iterator} do`)
222 | return Block.new(self)
223 | end
224 |
225 | function Block.Compare(self: Block, Left: string, Right: string, Operator: EqualityOperator): Block
226 | self:Line(`if {Left} {Operators[Operator]} {Right} then`)
227 | return Block.new(self)
228 | end
229 |
230 | function Block.Branch(self: Block, Branch: BranchType, Left: string?, Right: string?, Operator: EqualityOperator?): Block
231 | local Parent = self.Parent
232 | assert(Parent, "Cannot branch the top level block.")
233 |
234 | --> Push previous branch content
235 | self:_appendOperations()
236 | Parent:Lines(self.Content)
237 |
238 | --> Create new branch
239 | if Branch == "Conditional" then
240 | Parent:Line(`elseif {Left} {Operators[Operator]} {Right} then`)
241 | else
242 | Parent:Line(`else`)
243 | end
244 |
245 | return Block.new(Parent)
246 | end
247 |
248 | function Block.Return(self: Block, Return: string): Block
249 | self:Line(`return {Return}`)
250 | return self
251 | end
252 |
253 | function Block.End(self: Block): Block
254 | self:_appendOperations()
255 |
256 | local Parent = self.Parent
257 | if Parent then
258 | Parent:Lines(self.Content)
259 | Parent:Line("end")
260 | return Parent
261 | end
262 |
263 | self:Line("end", 0)
264 | return self
265 | end
266 |
267 | function Block.Wrap(self: Block, Front: string, Back: string): Block
268 | local First = self.Content[1]
269 | self.Content[1] = `{Front}{First}`
270 |
271 | local Last = self.Content[#self.Content]
272 | self.Content[#self.Content] = `{string.gsub(Last, "\n", "")}{Back}\n`
273 |
274 | return self
275 | end
276 |
277 | function Block.Unwrap(self: Block)
278 | return table.concat(self.Content)
279 | end
280 |
281 | local Function = {}
282 | Function.__index = Function
283 | setmetatable(Function, Block)
284 |
285 | function Function.new(Name: string, Arguments: string, Return: string?, IsInlined: boolean?, Localised: boolean?): Block
286 | local Block = Block.new(nil)
287 | setmetatable(Block, Function)
288 |
289 | local Suffix = Return and `: {Return}` or ""
290 | Block:Advance(1)
291 |
292 | if IsInlined then
293 | Block:Line(`{Name} = function({Arguments}){Suffix}`, 0)
294 | elseif Localised then
295 | Block:Line(`local function {Name}({Arguments}){Suffix}`, 0)
296 | else
297 | Block:Line(`function {Name}({Arguments}){Suffix}`, 0)
298 | end
299 |
300 | return Block
301 | end
302 |
303 | local Connection = {}
304 | Connection.__index = Connection
305 | setmetatable(Connection, Block)
306 |
307 | function Connection.new(Signal: string, Arguments: string): Block
308 | local Block = Block.new()
309 | setmetatable(Block, Connection)
310 | Block:Advance(1)
311 | Block:Line(`{Signal}:Connect(function({Arguments})`, 0)
312 | return Block
313 | end
314 |
315 | function Connection.End(self: Block, Return: string?): Block
316 | self:Line("end)", 0)
317 | return self
318 | end
319 |
320 | return {
321 | Block = Block.new,
322 | Function = Function.new,
323 | Connection = Connection.new
324 | }
--------------------------------------------------------------------------------
/src/Generator/Typescript.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | local Parser = require("../Parser")
4 | local Settings = require("../Settings")
5 |
6 | local Util = require("./Util")
7 | local Blocks = require("./Blocks")
8 | local Prefabs = require("./Prefabs")
9 |
10 | local Builder = require("../Modules/Builder")
11 |
12 | type Type = Parser.TypeNode
13 | type Scope = Parser.Scope
14 | type Context = "Client" | "Server" | "Shared"
15 | type AbstractSyntaxTree = Parser.Body
16 |
17 | type State = {
18 | Scope: Scope,
19 | Indent: number,
20 | Casing: Settings.Cases,
21 | Builder: Builder.Builder,
22 | Context: Context,
23 | Options: Parser.Options,
24 | }
25 |
26 | local TESTING_HEADER = "type Instance = number\ntype Player = number\ntype CFrame = number\ntype Vector3 = number\ntype Color3 = number\ntype Sound = number\ntype buffer = number\n"
27 | local VERSION_HEADER = `// File generated by Blink v{_G.VERSION or "0.0.0"} (https://github.com/1Axen/Blink)\n// This file is not meant to be edited\n`
28 |
29 | local Generators = {}
30 | local Primitives = Prefabs.Primitives
31 |
32 | local function GetPrefix(Indent: number): string
33 | return Indent == 0 and "declare " or ""
34 | end
35 |
36 | local function IsValidIdentifier(Identifier: string): boolean
37 | return Identifier:match("%a%w*") == Identifier
38 | end
39 |
40 | local function FormatKey(Key: string): string
41 | if IsValidIdentifier(Key) then
42 | return Key
43 | end
44 | return `["{Key}"]`
45 | end
46 |
47 | local function GetTypescriptType(State: State, Declaration: Type): (string, string)
48 | local Type = ""
49 | local Values = ""
50 |
51 | if Declaration.Type == "Primitive" then
52 | local Tokens = (Declaration :: Parser.Primitive).Tokens
53 | local Primitive = Primitives[Tokens.Primitive.Value]
54 | if type(Primitive.Type) == "function" then
55 | Type = Primitive.Type(Declaration :: Parser.Primitive)
56 | else
57 | Type = Primitive.Type
58 | end
59 |
60 | if Type == "any" then
61 | Type = "unknown"
62 | end
63 | elseif Declaration.Type == "Set" then
64 | local Value = (Declaration :: Parser.Set).Value
65 | local Entries: {string} = {}
66 |
67 | for _, Flag in Value.Values do
68 | table.insert(Entries, `{FormatKey(Flag)}: boolean,`)
69 | end
70 |
71 | Type = `\{{table.concat(Entries)}\}`
72 | elseif Declaration.Type == "Enum" then
73 | local Value = (Declaration :: Parser.Enum).Value
74 | Type = Util.GenerateEnumLiterals(Value.Values)
75 | elseif Declaration.Type == "TagEnum" then
76 | local Value = (Declaration :: Parser.TagEnum).Value
77 | local Literals = {}
78 |
79 | for Index, Variant in Value.Values do
80 | local TagField = `{Value.Tag}: "{Variant.Name}"`
81 | local ValueType = GetTypescriptType(State, Variant)
82 | table.insert(Literals, `\{ {TagField},{string.sub(ValueType, 2)}`)
83 | end
84 |
85 | Type = table.concat(Literals, " | ")
86 | elseif Declaration.Type == "Map" then
87 | local MapValue = (Declaration :: Parser.Map).Value
88 | local Key = GetTypescriptType(State, MapValue.Values[1])
89 | local Value = GetTypescriptType(State, MapValue.Values[2])
90 | Type = `Map<{Key}, {Value}>`
91 | elseif Declaration.Type == "Struct" then
92 | local Value = Declaration.Value
93 | local Fields = {}
94 |
95 | for _, Field in Value.Values do
96 | local Name = Field.Name
97 | local FieldType = GetTypescriptType(State, Field)
98 | local Optional = Field.Type == "Optional" and "?" or ""
99 | table.insert(Fields, `{FormatKey(Name)}{Optional}: {FieldType}`)
100 | end
101 |
102 | Type = `\{ {table.concat(Fields, ", ")}\ }`
103 | elseif Declaration.Type == "Generic" then
104 | Type = `{Declaration.Value.Generic}`
105 | elseif Declaration.Type == "Tuple" then
106 | local Value = (Declaration :: Parser.Tuple).Value
107 | local TupleValues = Value.Values
108 |
109 | local Types = {}
110 | local Variables = {}
111 | local VariableTypes = {}
112 |
113 | for Index, TupleValue in TupleValues do
114 | local Variable = `Value{Index}`
115 | local ValueType = GetTypescriptType(State, TupleValue)
116 | table.insert(Types, ValueType)
117 | table.insert(Variables, Variable)
118 | table.insert(VariableTypes, `{Variable}: {ValueType}`)
119 | end
120 |
121 | Type = `{table.concat(Types, ", ")}`
122 | Values = table.concat(VariableTypes, ", ")
123 | elseif Declaration.Type == "Array" then
124 | local Value = Declaration.Value
125 | Type = `{GetTypescriptType(State, Value.Of)}[]`
126 | elseif Declaration.Type == "Optional" then
127 | local Value = Declaration.Value
128 | Type = `{GetTypescriptType(State, Value.Of)} | undefined`
129 | end
130 |
131 | local Value = Declaration.Value
132 | if Value.Parameters then
133 | local Parameters = {}
134 |
135 | --> Generate luau types for parameters
136 | for Index, Parameter in Value.Parameters do
137 | local ParameterType = GetTypescriptType(State, Parameter)
138 | table.insert(Parameters, ParameterType)
139 | end
140 |
141 | --> Wrap in chevrons
142 | Type = `{Type}<{table.concat(Parameters, ",")}>`
143 | end
144 |
145 | --> Generalized type generation, works for everything except tuples
146 | if Declaration.Type ~= "Tuple" then
147 | Values = `Value: {Type}`
148 | else
149 | Type = `LuaTuple<[{Type}]>`
150 | end
151 |
152 | return Type, Values
153 | end
154 |
155 | function Generators.Type(State: State, Type: Type)
156 | local Name = Type.Name
157 | local Value = Type.Value
158 |
159 | local Indent = State.Indent
160 | local StringBuilder = State.Builder
161 |
162 | local TypescriptType = GetTypescriptType(State, Type)
163 |
164 | local Generics = ""
165 | if Value.Generics then
166 | local Types = {}
167 | for _, Generic in Value.Generics.List do
168 | table.insert(Types, Generic)
169 | end
170 |
171 | Generics = `<{table.concat(Types, ",")}>`
172 | end
173 |
174 | StringBuilder.Push(`type {Name}{Generics} = {TypescriptType}`, 0, Indent)
175 | end
176 |
177 | function Generators.Export(State: State, Type: Type)
178 | local Name = Type.Name
179 |
180 | local Indent = State.Indent
181 | local Casing = State.Casing
182 | local StringBuilder = State.Builder
183 |
184 | local Types, Values = GetTypescriptType(State, Type)
185 | StringBuilder.Push(`{GetPrefix(Indent)}const {Name}: \{`, 0, Indent)
186 | StringBuilder.Push(`{Casing.Read}: (Buffer: buffer) => {Types}`, 0, Indent + 1)
187 | StringBuilder.Push(`{Casing.Write}: ({Values}) => buffer`, 0, Indent + 1)
188 | StringBuilder.Push(`\}`, 0, Indent)
189 | end
190 |
191 | function Generators.Function(State: State, Function: Parser.Function)
192 | local Name = Function.Name
193 | local Value = Function.Value
194 |
195 | local Indent = State.Indent
196 | local Casing = State.Casing
197 | local StringBuilder = State.Builder
198 |
199 | local _, Values = "void", "Value: void"
200 | if Value.Data then
201 | _, Values = GetTypescriptType(State, Value.Data)
202 | end
203 |
204 | local ReturnTypes = "void"
205 | if Value.Return then
206 | ReturnTypes = GetTypescriptType(State, Value.Return)
207 | end
208 |
209 | StringBuilder.Push(`export {GetPrefix(Indent)}const {Name}: \{`, 0, Indent)
210 |
211 | if State.Context == "Server" then
212 | StringBuilder.Push(`{Casing.On}: (Listener: (Player: Player, {Values}) => {ReturnTypes}) => void`, 0, Indent + 1)
213 | else
214 | if Value.Yield == "Promise" then
215 | ReturnTypes = `Promise<{ReturnTypes}>`
216 | end
217 |
218 | StringBuilder.Push(`{Casing.Invoke}: ({Values}) => {ReturnTypes}`, 0, Indent + 1)
219 | end
220 |
221 | StringBuilder.Push(`\}`, 0, Indent)
222 | end
223 |
224 | function Generators.Event(State: State, Event: Parser.Event)
225 | local Name = Event.Name
226 | local Value = Event.Value
227 |
228 | local Indent = State.Indent
229 | local Casing = State.Casing
230 | local StringBuilder = State.Builder
231 |
232 | local IsPolling = if Value.Poll ~= nil then Value.Poll else State.Options.UsePolling
233 |
234 | local _, Values = "", "Value: void"
235 | if Value.Data then
236 | _, Values = GetTypescriptType(State, Value.Data)
237 | end
238 |
239 | local ListenerValues = Values
240 | if Value.From == "Client" then
241 | ListenerValues = `Player: Player, {Values}`
242 | end
243 |
244 | if IsPolling then
245 | ListenerValues = `Index: number, {ListenerValues}`
246 | ListenerValues = `LuaTuple<[{ListenerValues}]>`
247 | end
248 |
249 | StringBuilder.Push(`export {GetPrefix(Indent)}const {Name}: \{`, 0, Indent)
250 |
251 | if Value.From == State.Context then
252 | if Value.From == "Server" then
253 | StringBuilder.Push(`{Casing.Fire}: (Player: Player, {Values}) => void`, 0, Indent + 1)
254 | StringBuilder.Push(`{Casing.FireAll}: ({Values}) => void`, 0, Indent + 1)
255 | StringBuilder.Push(`{Casing.FireExcept}: (Except: Player, {Values}) => void`, 0, Indent + 1)
256 | StringBuilder.Push(`{Casing.FireList}: (List: Player[], {Values}) => void`, 0, Indent + 1)
257 | else
258 | StringBuilder.Push(`{Casing.Fire}: ({Values}) => void`, 0, Indent + 1)
259 | end
260 | elseif IsPolling then
261 | StringBuilder.Push(`{Casing.Iter}: () => IterableFunction<{ListenerValues}>`, 0, Indent + 1)
262 | else
263 | StringBuilder.Push(`{Casing.On}: (Listener: ({ListenerValues}) => void) => (() => void)`, 0, Indent + 1)
264 | end
265 |
266 | StringBuilder.Push(`\}`, 0, Indent)
267 | end
268 |
269 | function Generators.Scope(State: State, Scope: Parser.ScopeNode)
270 | local Name = Scope.Name
271 | local Value = Scope.Value
272 |
273 | local Parent = State.Scope
274 | local Indent = State.Indent
275 | local StringBuilder = State.Builder
276 |
277 | State.Indent += 1
278 | State.Scope = Value.Scope
279 |
280 | StringBuilder.Push(`export {GetPrefix(Indent)}namespace {Name} \{`, 0, Indent)
281 | Generators.Tree(State, Value.Values)
282 | StringBuilder.Push(`}`, 0, Indent)
283 |
284 | State.Indent -= 1
285 | State.Scope = Parent
286 | end
287 |
288 | function Generators.Tree(State: State, Tree: {Parser.Declaration})
289 | for Index, Declaration in Tree do
290 | if Declaration.Type == "Scope" then
291 | Generators.Scope(State, Declaration :: Parser.ScopeNode)
292 | elseif Declaration.Type == "Event" then
293 | Generators.Event(State, Declaration :: Parser.Event)
294 | elseif Declaration.Type == "Function" then
295 | Generators.Function(State, Declaration :: Parser.Function)
296 | else
297 | if Declaration.Value.Export then
298 | Generators.Export(State, Declaration :: Type)
299 | end
300 |
301 | Generators.Type(State, Declaration :: Type)
302 | end
303 | end
304 | end
305 |
306 | return function(FileContext: Context, AbstractSyntaxTree: AbstractSyntaxTree): string
307 | local Options = AbstractSyntaxTree.Value.Options
308 | local Casing = Settings.GetCasing(Options.Casing or "Pascal" :: any)
309 | local StringBuilder = Builder.new()
310 |
311 | StringBuilder.Push(VERSION_HEADER)
312 | StringBuilder.Push(`export declare const {Casing.StepReplication}: () => void`)
313 |
314 | if _G.VERSION == nil then
315 | StringBuilder.Push(TESTING_HEADER)
316 | end
317 |
318 | local State: State = {
319 | Scope = nil :: any,
320 | Indent = 0,
321 | Casing = Casing,
322 | Context = FileContext,
323 | Options = Options,
324 | Builder = StringBuilder,
325 | }
326 |
327 | Generators.Tree(State, AbstractSyntaxTree.Value.Declarations)
328 |
329 | return table.concat(StringBuilder.DumpLines())
330 | end
331 |
--------------------------------------------------------------------------------
/src/Generator/Util.luau:
--------------------------------------------------------------------------------
1 | local Parser = require("../Parser")
2 |
3 | type Scope = Parser.Scope
4 |
5 | local function GetScopePrefix(ReferenceScope: Scope?, Dots: boolean?, CurrentScope: Scope): string
6 | local Prefix = ""
7 | local WorkingScope: Scope? = ReferenceScope or CurrentScope
8 | while (WorkingScope and WorkingScope.Name.Value ~= "") do
9 | Prefix = `{WorkingScope.Name.Value}{Dots and "." or "_"}` .. Prefix
10 | WorkingScope = WorkingScope.Parent
11 | end
12 |
13 | return Prefix
14 | end
15 |
16 | local function GetScopeIndent(CurrentScope: Scope): number
17 | return math.max(#string.split(GetScopePrefix(nil, true, CurrentScope), "."), 1)
18 | end
19 |
20 | local function GetTypesPath(Name: string, Write: boolean, CurrentScope: Scope): string
21 | return `{GetScopePrefix(nil, false, CurrentScope)}{Write and "Write" or "Read"}{Name}`
22 | end
23 |
24 | local function GetExportName(Name: string, CurrentScope: Scope): string
25 | return `{GetScopePrefix(nil, false, CurrentScope)}{Name}`
26 | end
27 |
28 | local function GenerateEnumLiterals(Variants: { string }): string
29 | local Literals = {}
30 | for Index, EnumItem in Variants do
31 | table.insert(Literals, `"{EnumItem}"`)
32 | end
33 | return `{table.concat(Literals, " | ")}`
34 | end
35 |
36 | return {
37 | GetPath = GetTypesPath,
38 | GetExportName = GetExportName,
39 | GetScopePrefix = GetScopePrefix,
40 | GetScopeIndent = GetScopeIndent,
41 | GenerateEnumLiterals = GenerateEnumLiterals,
42 | }
--------------------------------------------------------------------------------
/src/Lexer.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | --!optimize 2
3 |
4 | local Error = require("./Modules/Error")
5 | local Settings = require("./Settings")
6 |
7 | export type Types =
8 | "Comma" | "OpenParentheses" | "CloseParentheses" | "OpenBraces" | "CloseBraces" | "OpenBrackets" | "CloseBrackets" | "Merge" --> Structs & enums
9 | | "String" | "Boolean" | "Number" --> Literals
10 | | "Array" | "Range" | "Optional" | "Class" | "Component" | "OpenChevrons" | "CloseChevrons" --> Attributes
11 | | "Assign" | "FieldAssign" | "Keyword" | "Primitive" | "Identifier" --> Reserved
12 | | "Import" | "As" --> Imports
13 | | "Whitespace" | "Comment" | "Unknown" | "EndOfFile"
14 |
15 | export type Keywords = "type" | "enum" | "struct" | "event" | "function"
16 |
17 | export type Token = {
18 | Type: Types,
19 | Value: string,
20 |
21 | Start: number,
22 | End: number,
23 | }
24 |
25 | export type Mode = "Parsing" | "Highlighting"
26 |
27 | local DOTS = "%.%."
28 | local NUMBER = "%-?%d*%.?%d+"
29 |
30 | local Keywords = Settings.Keywords
31 | local Primitives = Settings.Primtives
32 |
33 | local Booleans = {
34 | ["true"] = true,
35 | ["false"] = true
36 | }
37 |
38 | local TOKENS = {
39 | --> Simple patterns
40 | {"^%s+", "Whitespace"},
41 | {"^=", "Assign"},
42 | {"^:", "FieldAssign"},
43 | {"^{", "OpenBraces"},
44 | {"^}", "CloseBraces"},
45 | {"^<", "OpenChevrons"},
46 | {"^>", "CloseChevrons"},
47 | {"^,", "Comma"},
48 | {"^%.%.", "Merge"},
49 |
50 | --> Comments
51 | {"^%-%-%[(=*)%[.-%]%1%]", "Comment"},
52 | {"^%-%-%[%[.-.*", "Comment"},
53 | {"^%-%-.-\n", "Comment"},
54 | {"^%-%-.-.*", "Comment"},
55 |
56 | --> Attribute patterns
57 | {"^?", "Optional"},
58 | {`^%(%a+%)`, "Class"},
59 | {`^%[]`, "Array"},
60 | {`^%({NUMBER}%)`, "Range"},
61 | {`^%({NUMBER}{DOTS}%)`, "Range"},
62 | {`^%({DOTS}{NUMBER}%)`, "Range"},
63 | {`^%({NUMBER}{DOTS}{NUMBER}%)`, "Range"},
64 | {`^%[{NUMBER}%]`, "Array"},
65 | {`^%[{NUMBER}{DOTS}%]`, "Array"},
66 | {`^%[{DOTS}{NUMBER}%]`, "Array"},
67 | {`^%[{NUMBER}{DOTS}{NUMBER}%]`, "Array"},
68 |
69 | {"^%(", "OpenParentheses"},
70 | {"^%)", "CloseParentheses"},
71 | {"^%[", "OpenBrackets"},
72 | {"^%]", "CloseBrackets"},
73 |
74 | --> String patterns
75 | {"^\"\"", function(Toke: string)
76 | return "String", ""
77 | end},
78 |
79 | {[[^(['"]).-[^\](\*)%2%1]], function(Token: string)
80 | return "String", string.sub(Token, 2, #Token - 1)
81 | end},
82 |
83 | {"^(['\"]).-.*", function(Token: string)
84 | return "String", string.sub(Token, 2)
85 | end},
86 |
87 | --> Complex patterns
88 | {"^[%w_]+%.[%w_%.]+", "Identifier"},
89 | {"^[%a_][%w_]*", function(Token: string)
90 | if Token == "import" then
91 | return "Import", Token
92 | elseif Token == "as" then
93 | return "As", Token
94 | elseif Keywords[Token] then
95 | return "Keyword", Token
96 | elseif Primitives[Token] then
97 | return "Primitive", Token
98 | elseif Booleans[Token] then
99 | return "Boolean", (Token == "true")
100 | end
101 |
102 | return "Identifier", Token
103 | end},
104 | }
105 |
106 | local SKIPPED_TOKENS = {
107 | Comment = true,
108 | Whitespace = true
109 | }
110 |
111 | local Lexer = {}
112 | Lexer.__index = Lexer
113 |
114 | export type Lexer = typeof(setmetatable({} :: {
115 | Mode: Mode,
116 | Size: number,
117 | Source: string,
118 | Cursor: number,
119 | }, Lexer))
120 |
121 | function Lexer.new(Mode: Mode?): Lexer
122 | return setmetatable({
123 | Size = 0,
124 | Mode = Mode or "Parsing",
125 | Source = "",
126 | Cursor = 1
127 | } :: any, Lexer)
128 | end
129 |
130 | function Lexer.Initialize(self: Lexer, Source: string)
131 | self.Size = #Source
132 | self.Source = Source
133 | self.Cursor = 1
134 | end
135 |
136 | function Lexer.GetNextToken(self: Lexer, DontAdvanceCursor: boolean?, StartAt: number?): Token
137 | if self.Cursor > self.Size then
138 | return {
139 | Type = "EndOfFile",
140 | Value = "",
141 | Start = #self.Source,
142 | End = #self.Source
143 | }
144 | end
145 |
146 | local Source = self.Source
147 | local Position = StartAt or self.Cursor
148 | local IsHighlighting = (self.Mode == "Highlighting")
149 |
150 | local function Match(Pattern: string): (string?, number, number)
151 | local Start, End = string.find(Source, Pattern, Position)
152 | if not Start or not End then
153 | return nil, Position, Position
154 | end
155 |
156 | local Text = string.sub(Source, Start, End)
157 | return Text, Position, math.min(Position + #Text, self.Size)
158 | end
159 |
160 | for Index, Token in TOKENS do
161 | local Pattern = Token[1]
162 | local Type: (Types | (Text: string) -> Types)? = Token[2]
163 |
164 | local Text, Start, End = Match(Pattern)
165 |
166 | --> Couldn't match this pattern, continue.
167 | if not Text then
168 | continue
169 | end
170 |
171 | if (not DontAdvanceCursor or (SKIPPED_TOKENS[Type] and not IsHighlighting)) then
172 | Position += #Text
173 | self.Cursor = Position
174 | end
175 |
176 | --> Whitespace matched, skip token.
177 | --> We don't want to skip whitespaces in highlighting mode.
178 | if SKIPPED_TOKENS[Type] and not IsHighlighting then
179 | return self:GetNextToken(DontAdvanceCursor)
180 | end
181 |
182 | if type(Type) == "function" then
183 | --> Only overwrite the type when highlighting
184 | local TrueType, TrueText = Type(Text)
185 |
186 | Type = TrueType
187 | Text = IsHighlighting and Text or TrueText
188 | end
189 |
190 | return {
191 | Type = Type,
192 | Value = Text,
193 | Start = Start,
194 | End = End,
195 | }
196 | end
197 |
198 | if not IsHighlighting then
199 | Error.new(Error.LexerUnexpectedToken, self.Source, "Unexpected token")
200 | :Primary({Start = self.Cursor, End = self.Cursor}, `Unexpected token`)
201 | :Emit()
202 | end
203 |
204 | --> Attempt to recover the lexer
205 | local Symbol = string.sub(self.Source, Position, Position)
206 | if DontAdvanceCursor ~= true then
207 | self.Cursor += 1
208 | end
209 |
210 | return {
211 | Type = "Unknown",
212 | Value = Symbol,
213 | Start = Position,
214 | End = Position,
215 | }
216 | end
217 |
218 | return Lexer
--------------------------------------------------------------------------------
/src/Modules/Builder.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | --!optimize 2
3 |
4 | local Builder = {}
5 |
6 | function Builder.new(): Builder
7 | local Strings = table.create(512)
8 |
9 | local function PushLines(Lines: {string}, Returns: number?, Tabs: number?)
10 | local Size = #Lines
11 | local Last = Lines[Size]
12 |
13 | local Tabs = Tabs or 0
14 | local Returns = Returns or 0
15 |
16 | --> FAST PATH :SUNGLASSES:
17 | if Tabs == 0 and Returns == 0 then
18 | table.move(Lines, 1, Size, #Strings + 1, Strings)
19 | return
20 | end
21 |
22 | local Indentation = string.rep("\t", Tabs)
23 |
24 | if Last == "" then
25 | Size -= 1
26 | Lines[Size] = nil
27 | end
28 |
29 | for Index, Line in Lines do
30 | table.insert(Strings, `{Indentation}{Line}{Index == Size and string.rep("\n", Returns) or ""}`)
31 | end
32 | end
33 |
34 | return {
35 | Push = function(Text: string, Returns: number?, Tabs: number?)
36 | table.insert(Strings, `{string.rep("\t", Tabs or 0)}{Text}\n{string.rep("\n", Returns or 0)}`)
37 | end,
38 |
39 | PushFront = function(Text: string, Returns: number?, Tabs: number?)
40 | table.insert(Strings, 1, `{string.rep("\n", Returns or 1)}{Text}{string.rep("\t", Tabs or 0)}\n`)
41 | end,
42 |
43 | PushLines = PushLines,
44 | PushMultiline = function(Text: string, Returns: number?, Tabs: number?)
45 | PushLines(string.split(Text, "\n"), Returns, Tabs)
46 | end,
47 |
48 | Print = function()
49 | print(table.concat(Strings))
50 | end,
51 |
52 | DumpNoClear = function()
53 | return table.concat(Strings)
54 | end,
55 |
56 | Dump = function(): string
57 | local Text = table.concat(Strings)
58 | table.clear(Strings)
59 | return Text
60 | end,
61 |
62 | DumpLines = function(): {string}
63 | return Strings
64 | end
65 | }
66 | end
67 |
68 | export type Builder = typeof(Builder.new())
69 |
70 | return Builder
--------------------------------------------------------------------------------
/src/Modules/Error.luau:
--------------------------------------------------------------------------------
1 | --!native
2 | --!optimize 2
3 |
4 | local IS_ROBLOX = (game ~= nil)
5 |
6 | local stdio;
7 | local process;
8 | if not IS_ROBLOX then
9 | stdio = require("@lune/stdio")
10 | process = require("@lune/process")
11 | end
12 |
13 | type Slice = {
14 | Line: number,
15 | Text: string,
16 | Spaces: number,
17 | Underlines: number,
18 | }
19 |
20 | export type Label = {
21 | Span: Span,
22 | Text: string,
23 | }
24 |
25 | type Color = "red" | "green" | "blue" | "black" | "white" | "cyan" | "purple" | "yellow" | "reset"
26 |
27 | export type Span = {
28 | Start: number,
29 | End: number,
30 | }
31 |
32 | local SpecialSymbols = {
33 | CLI = {
34 | Line = "─",
35 | Down = "│",
36 | DownDash = "┆",
37 | Cross = "┬",
38 | RightUp = "╭",
39 | LeftDown = "╯",
40 | RightDown = "╰"
41 | },
42 | ROBLOX = {
43 | Line = "-",
44 | Down = "|",
45 | DownDash = " ",
46 | Cross = "-",
47 | RightUp = "╭",
48 | LeftDown = "╯",
49 | RightDown = "╰"
50 | }
51 | }
52 |
53 | local Colors = {
54 | black = "rgb(0, 0, 0)",
55 | blue = "rgb(0, 0, 255)",
56 | cyan = "rgb(0, 255, 255)",
57 | green = "rgb(0, 255, 0)",
58 | purple = "rgb(163, 44, 196)",
59 | red = "rgb(255, 0, 0)",
60 | white = "rgb(255, 255, 255)",
61 | yellow = "rgb(255, 255, 0)",
62 | }
63 |
64 | local Compact = false
65 |
66 | local Error = {
67 | OnEmit = nil,
68 |
69 | LexerUnexpectedToken = 1001,
70 |
71 | ParserUnexpectedEndOfFile = 2001,
72 | ParserUnexpectedToken = 2002,
73 | ParserUnknownOption = 2003,
74 | ParserUnexpectedSymbol = 2004,
75 | ParserExpectedExtraToken = 2005,
76 |
77 | AnalyzeReferenceInvalidType = 3001,
78 | AnalyzeInvalidOptionalType = 3002,
79 | AnalyzeNestedMap = 3003,
80 | AnalyzeDuplicateField = 3004,
81 | AnalyzeReservedIdentifier = 3005,
82 | AnalyzeNestedScope = 3006,
83 | AnalyzeUnknownReference = 3007,
84 | AnalyzeInvalidRangeType = 3008,
85 | AnalyzeInvalidRange = 3009,
86 | AnalyzeDuplicateDeclaration = 3010,
87 | AnalyzeDuplicateTypeGeneric = 3011,
88 | AnalyzeInvalidGenerics = 3012,
89 | AnalyzeInvalidExport = 3013,
90 | AnalyzeUnknownImport = 3014,
91 | AnalyzeErrorWhileImporting = 3015,
92 | AnalyzeOptionAfterStart = 3016,
93 | }
94 |
95 | Error.__index = Error
96 |
97 | export type Class = typeof(setmetatable({} :: {
98 | Labels: {Label},
99 | Source: {string},
100 | Message: string,
101 |
102 | Code: number,
103 | File: string,
104 | RawMessage: string,
105 | PrimarySpan: Span?,
106 | }, Error))
107 |
108 | local Symbols = IS_ROBLOX and SpecialSymbols.ROBLOX or SpecialSymbols.CLI
109 |
110 | local function Color(Text: string, Color: Color): string
111 | if IS_ROBLOX then
112 | return `{Text}`
113 | end
114 |
115 | return `{stdio.color(Color)}{Text}{stdio.color("reset")}`
116 | end
117 |
118 | local function Style(Text: string, Style: "bold" | "dim"): string
119 | if IS_ROBLOX then
120 | return Text
121 | end
122 |
123 | return `{stdio.style(Style)}{Text}{stdio.style("reset")}`
124 | end
125 |
126 | function Error.SetCompact(Value: boolean)
127 | Compact = Value
128 | end
129 |
130 | function Error.GetNameFromCode(From: number): string
131 | for Name, Code in Error do
132 | if Code == From then
133 | return Name
134 | end
135 | end
136 |
137 | error(`Unknown error code: {From}`)
138 | end
139 |
140 | function Error.new(Code: number, Source: string, Message: string, File: string?): Class
141 | local Lines = string.split(Source, "\n")
142 | local Content = `{Color(`[E{string.format("%04i", Code)}] Error`, "red")}: {Message}`
143 | Content ..= `\n {Symbols.RightUp}{Symbols.Line}[{File or "input.blink"}:1:{#Lines}]`
144 | Content ..= `\n {Symbols.Down}`
145 |
146 | return setmetatable({
147 | Labels = {},
148 | Source = Lines,
149 | Message = Content,
150 |
151 | Code = Code,
152 | File = File or "input.blink",
153 | RawMessage = Message
154 | }, Error)
155 | end
156 |
157 | function Error.Slice(self: Class, Span: Span): {Slice}
158 | local Slices = {}
159 | local Cursor = 1
160 |
161 | for Line, Text in self.Source do
162 | local Start = Cursor
163 | local Length = #Text
164 |
165 | --> Advance cursor
166 | --> Cursor + #Length = \n => \n + 1 = Next line
167 | Cursor += (Length + 1)
168 |
169 | --> Span end is past cursor
170 | if Span.Start > Cursor then
171 | continue
172 | end
173 |
174 | local Spaces = (Span.Start - Start)
175 | local Underlines = math.clamp(Span.End - Span.Start, 1, Length)
176 |
177 | table.insert(Slices, {
178 | Line = Line,
179 | Text = Text,
180 | Spaces = Spaces,
181 | Underlines = Underlines
182 | })
183 |
184 | if Span.End <= Cursor then
185 | break
186 | end
187 | end
188 |
189 | return Slices
190 | end
191 |
192 | function Error.Label(self: Class, Span: Span, Text: string, TextColor: Color): Class
193 | local Slices = self:Slice(Span)
194 |
195 | --> Analyze hook
196 | table.insert(self.Labels, {
197 | Span = Span,
198 | Text = Text,
199 | })
200 |
201 | --> Construct message
202 | for Index, Slice in Slices do
203 | self.Message ..= `\n{Style(string.format("%03i", Slice.Line), "dim")} {Symbols.Down} {Slice.Text}`
204 | if Index == #Slices then
205 | local Length = (Slice.Underlines // 2)
206 | local Indent = ` {Symbols.DownDash} {string.rep(" ", Slice.Spaces)}`
207 | local Underlines = Color(string.rep(Symbols.Line, Length), TextColor)
208 | local ExtraIndent = string.rep(" ", Length)
209 |
210 | self.Message ..= `\n{Indent}{Underlines}{Color(Symbols.Cross, TextColor)}{Underlines}`
211 | self.Message ..= `\n{Indent}{ExtraIndent}{Color(Symbols.Down, TextColor)}`
212 | self.Message ..= `\n{Indent}{ExtraIndent}{Color(`{Symbols.RightDown}{Symbols.Line}{Symbols.Line}`, TextColor)} {Color(Text, TextColor)}`
213 | end
214 | end
215 |
216 | return self
217 | end
218 |
219 | function Error.Primary(self: Class, Span: Span, Text: string): Class
220 | self.PrimarySpan = Span
221 | return self:Label(Span, Text, "red")
222 | end
223 |
224 | function Error.Secondary(self: Class, Span: Span, Text: string): Class
225 | return self:Label(Span, Text, "blue")
226 | end
227 |
228 | function Error.Emit(self: Class): never
229 | self.Message ..= `\n {Symbols.Down}`
230 | self.Message ..= `\n{string.rep(Symbols.Line, 4)}{Symbols.LeftDown}\n`
231 |
232 | if IS_ROBLOX or _G.BUNDLED == nil then
233 | local OnEmit: ((Class) -> ())? = Error.OnEmit
234 | if OnEmit then
235 | OnEmit(self)
236 | end
237 |
238 | error(self.Message, 2)
239 | elseif stdio and process then
240 | if Compact then
241 | local LineStart = 0
242 | local LineFinish = 0
243 |
244 | if self.PrimarySpan then
245 | local Slices = self:Slice(self.PrimarySpan)
246 | LineStart = Slices[1].Line
247 | LineFinish = Slices[#Slices].Line
248 | end
249 |
250 | stdio.ewrite(string.format(
251 | "[E%04i] [L%03i:L%03i] [%s] Error: %s",
252 | self.Code,
253 | LineStart,
254 | LineFinish,
255 | self.File,
256 | self.RawMessage
257 | ))
258 | else
259 | stdio.ewrite(self.Message)
260 | end
261 |
262 | if _G.WATCH_THREAD then
263 | error(self.RawMessage)
264 | else
265 | process.exit(1)
266 | end
267 | end
268 |
269 | error("never")
270 | end
271 |
272 | return Error
--------------------------------------------------------------------------------
/src/Modules/Format.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | local stdio = require("@lune/stdio")
4 |
5 | local function Style(text: any, style: stdio.Style): string
6 | return `{stdio.style(style)}{text}{stdio.style("reset")}`
7 | end
8 |
9 | local function Color(text: any, color: stdio.Color): string
10 | return `{stdio.color(color)}{text}{stdio.color("reset")}`
11 | end
12 |
13 | local KEYWORDS = {
14 | ["and"] = true,
15 | ["break"] = true,
16 | ["do"] = true,
17 | ["else"] = true,
18 | ["elseif"] = true,
19 | ["end"] = true,
20 | ["false"] = true,
21 | ["for"] = true,
22 | ["function"] = true,
23 | ["if"] = true,
24 | ["in"] = true,
25 | ["local"] = true,
26 | ["nil"] = true,
27 | ["not"] = true,
28 | ["or"] = true,
29 | ["repeat"] = true,
30 | ["return"] = true,
31 | ["then"] = true,
32 | ["true"] = true,
33 | ["until"] = true,
34 | ["while"] = true
35 | }
36 |
37 | local function ShouldntWrap(value: any): boolean
38 | if type(value) ~= "string" then
39 | return false
40 | end
41 |
42 | if #value == 0 then
43 | return false
44 | end
45 |
46 | if string.find(value, "[^%d%a_]") then
47 | return false
48 | end
49 |
50 | if tonumber(string.sub(value, 1, 1)) then
51 | return false
52 | end
53 |
54 | if KEYWORDS[value] then
55 | return false
56 | end
57 |
58 | return true
59 | end
60 |
61 |
62 | local Cache
63 | local INDENT = string.rep(" ", 3)
64 |
65 | local function Noop()
66 |
67 | end
68 |
69 | local function Format(Value: any, Filter: ((Value: any) -> boolean)?, Depth: number?): string
70 | local Depth = Depth or 0
71 | local Filter = (Filter or Noop) :: (Value: any) -> boolean
72 |
73 | if Depth == 0 then
74 | Cache = {}
75 | end
76 |
77 | if Filter(Value) == false then
78 | return ""
79 | end
80 |
81 | local Type = type(Value)
82 | if Type == "string" then
83 | return Color(`"{Value}"`, "green")
84 | elseif Type == "number" then
85 | if Value == math.huge then return Color("math.huge", "cyan") end
86 | if Value == -math.huge then return Color("-math.huge", "cyan") end
87 | return Color(Value, "cyan")
88 | elseif Type == "boolean" then
89 | return Color(Value, "yellow")
90 | elseif Type == "table" then
91 | local Address = string.match(tostring(Value), "0x[%w]+")
92 | if Cache[Value] then
93 | return `{Color("{CACHED}" , "red")} {Style(Address, "dim")}`
94 | end
95 |
96 | local Tabs = string.rep(INDENT, Depth)
97 | local Newline = ("\n" .. INDENT .. Tabs)
98 | local Text = Style(`\{ ({Address})`, "dim") .. Newline
99 |
100 | Cache[Value] = true
101 |
102 | local First = true
103 | for Key, Value in Value do
104 | if Filter(Key) == false then
105 | continue
106 | end
107 |
108 | local KeyText = ShouldntWrap(Key) and Key or `["{Format(Key, Filter, Depth + 1)}"]`
109 |
110 | if not First then
111 | Text ..= Style(",", "dim") .. Newline
112 | end
113 |
114 | First = false
115 | Text ..= `{KeyText} = {Format(Value, Filter, Depth + 1)}`
116 | end
117 |
118 | Text ..= "\n" .. Tabs .. Style("}", "dim")
119 | return Text
120 | end
121 |
122 | return `<{Type}>`
123 | end
124 |
125 | return Format
--------------------------------------------------------------------------------
/src/Modules/Path.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | ---- Imports ----
4 |
5 | ---- Settings ----
6 |
7 | ---- Constants ----
8 |
9 | local Utility = {}
10 |
11 | ---- Variables ----
12 |
13 | ---- Private Functions ----
14 |
15 | ---- Public Functions ----
16 |
17 | function Utility.Components(Path: string): (string?, string?, string?)
18 | local Filename: string?;
19 | local Directory, FilenameExt, Extension = string.match(Path, "(.-)([^\\/]-%.?([^%.\\/]*))$")
20 |
21 | if FilenameExt then
22 | if FilenameExt == Extension then
23 | Extension = nil
24 | Filename = FilenameExt
25 | elseif Extension then
26 | Filename = string.sub(FilenameExt, 1, #FilenameExt - (#Extension + 1))
27 | end
28 | end
29 |
30 | return Directory, Filename, Extension
31 | end
32 |
33 | function Utility.Filename(Path: string): string?
34 | local _, Filename = Utility.Components(Path)
35 | return Filename
36 | end
37 |
38 | function Utility.Directory(Path: string): string?
39 | local Directory = Utility.Components(Path)
40 | return Directory
41 | end
42 |
43 | function Utility.Extension(Path: string): string?
44 | local _, _, Extension = Utility.Components(Path)
45 | return Extension
46 | end
47 |
48 | function Utility.NameWithExtension(Path: string): string?
49 | local _, Name, Extension = Utility.Components(Path)
50 | if Name and Extension then
51 | return `{Name}.{Extension}`
52 | end
53 |
54 | return
55 | end
56 |
57 | ---- Initialization ----
58 |
59 | ---- Connections ----
60 |
61 | return Utility
--------------------------------------------------------------------------------
/src/Modules/Table.luau:
--------------------------------------------------------------------------------
1 | return {
2 | Merge = function(...: {any}): {any}
3 | local Tables = {...}
4 | local Allocate = 0
5 |
6 | for _, Table in Tables do
7 | Allocate += #Table
8 | end
9 |
10 | local Index = 1
11 | local Merged = table.create(Allocate)
12 |
13 | for _, Table in Tables do
14 | table.move(Table, 1, #Table, Index, Merged)
15 | Index += #Table
16 | end
17 |
18 | return Merged
19 | end,
20 | DeepClone = function(Table: {[any]: any}): {[any]: any}
21 | local Cache = {}
22 |
23 | local function Clone(Original)
24 | local Cached = Cache[Original]
25 | if Cached ~= nil then
26 | return Cached
27 | end
28 |
29 | local Copy = Original
30 | if type(Original) == "table" then
31 | Copy = {}
32 | Cache[Original] = Copy
33 |
34 | for Key, Value in Original do
35 | Copy[Clone(Key)] = Clone(Value)
36 | end
37 | end
38 |
39 | return Copy
40 | end
41 |
42 | return Clone(Table)
43 | end
44 | }
--------------------------------------------------------------------------------
/src/Settings.luau:
--------------------------------------------------------------------------------
1 | type NumberRange = {Min: number, Max: number}
2 | local NumberRange = {
3 | new = function(Min: number, Max: number?): NumberRange
4 | return {
5 | Min = Min,
6 | Max = Max or Min
7 | }
8 | end
9 | }
10 |
11 | export type Case = "Camel" | "Snake" | "Pascal"
12 | export type Cases = {
13 | Fire: string,
14 | FireAll: string,
15 | FireList: string,
16 | FireExcept: string,
17 | On: string,
18 | Invoke: string,
19 | StepReplication: string,
20 | Next: string,
21 | Iter: string,
22 | Read: string,
23 | Write: string,
24 | }
25 |
26 | export type Primitive = {
27 | Bounds: NumberRange?,
28 | Integer: boolean?,
29 | Component: boolean,
30 | AllowsRange: boolean,
31 | AllowsOptional: boolean,
32 | AllowedComponents: number,
33 | }
34 |
35 | local Indexes = {
36 | Pascal = 1,
37 | Camel = 2,
38 | Snake = 3
39 | }
40 |
41 | local Cases = {
42 | On = {"On", "on", "on"},
43 | Invoke = {"Invoke", "invoke", "invoke"},
44 | Fire = {"Fire", "fire", "fire"},
45 | FireAll = {"FireAll", "fireAll", "fire_all"},
46 | FireList = {"FireList", "fireList", "fire_list"},
47 | FireExcept = {"FireExcept", "fireExcept", "fire_except"},
48 | StepReplication = {"StepReplication", "stepReplication", "step_replication"},
49 | Next = {"Next", "next", "next"},
50 | Iter = {"Iter", "iter", "iter"},
51 | Read = {"Read", "read", "read"},
52 | Write = {"Write", "write", "write"},
53 | }
54 |
55 | local Primitives: {[string]: Primitive} = {
56 | u8 = {
57 | Bounds = NumberRange.new(0, 255),
58 | Integer = true,
59 | Component = true,
60 | AllowsRange = true,
61 | AllowsOptional = true,
62 | AllowedComponents = 0
63 | },
64 | u16 = {
65 | Bounds = NumberRange.new(0, 65535),
66 | Integer = true,
67 | Component = true,
68 | AllowsRange = true,
69 | AllowsOptional = true,
70 | AllowedComponents = 0
71 | },
72 | u32 = {
73 | Bounds = NumberRange.new(0, 4294967295),
74 | Integer = true,
75 | Component = true,
76 | AllowsRange = true,
77 | AllowsOptional = true,
78 | AllowedComponents = 0
79 | },
80 | i8 = {
81 | Bounds = NumberRange.new(-128, 127),
82 | Integer = true,
83 | Component = true,
84 | AllowsRange = true,
85 | AllowsOptional = true,
86 | AllowedComponents = 0
87 | },
88 | i16 = {
89 | Bounds = NumberRange.new(-32768, 32767),
90 | Integer = true,
91 | Component = true,
92 | AllowsRange = true,
93 | AllowsOptional = true,
94 | AllowedComponents = 0
95 | },
96 | i32 = {
97 | Bounds = NumberRange.new(-2147483648, 2147483647),
98 | Integer = true,
99 | Component = true,
100 | AllowsRange = true,
101 | AllowsOptional = true,
102 | AllowedComponents = 0
103 | },
104 | f16 = {
105 | Bounds = NumberRange.new(-65504, 65504),
106 | Component = true,
107 | AllowsRange = true,
108 | AllowsOptional = true,
109 | AllowedComponents = 0
110 | },
111 | f32 = {
112 | Bounds = NumberRange.new(-16777216, 16777216),
113 | Component = true,
114 | AllowsRange = true,
115 | AllowsOptional = true,
116 | AllowedComponents = 0
117 | },
118 | f64 = {
119 | Bounds = NumberRange.new(-2^53, 2^53),
120 | Component = true,
121 | AllowsRange = true,
122 | AllowsOptional = true,
123 | AllowedComponents = 0
124 | },
125 | boolean = {
126 | Component = false,
127 | AllowsRange = false,
128 | AllowsOptional = true,
129 | AllowedComponents = 0
130 | },
131 | string = {
132 | Bounds = NumberRange.new(0, 4294967295),
133 | Integer = true,
134 | Component = false,
135 | AllowsRange = true,
136 | AllowsOptional = true,
137 | AllowedComponents = 0
138 | },
139 | vector = {
140 | Component = false,
141 | AllowsRange = true,
142 | AllowsOptional = true,
143 | AllowedComponents = 1
144 | },
145 | buffer = {
146 | Bounds = NumberRange.new(0, 4294967295),
147 | Integer = true,
148 | Component = false,
149 | AllowsRange = true,
150 | AllowsOptional = true,
151 | AllowedComponents = 0
152 | },
153 | Color3 = {
154 | Component = false,
155 | AllowsRange = false,
156 | AllowsOptional = true,
157 | AllowedComponents = 0
158 | },
159 | CFrame = {
160 | Component = false,
161 | AllowsRange = false,
162 | AllowsOptional = true,
163 | AllowedComponents = 2
164 | },
165 | Instance = {
166 | Component = false,
167 | AllowsRange = false,
168 | AllowsOptional = true,
169 | AllowedComponents = 0
170 | },
171 | BrickColor = {
172 | Component = false,
173 | AllowsRange = false,
174 | AllowsOptional = true,
175 | AllowedComponents = 0
176 | },
177 | unknown = {
178 | Component = false,
179 | AllowsRange = false,
180 | AllowsOptional = false,
181 | AllowedComponents = 0
182 | },
183 | DateTime = {
184 | Component = false,
185 | AllowsRange = false,
186 | AllowsOptional = true,
187 | AllowedComponents = 0
188 | },
189 | DateTimeMillis = {
190 | Component = false,
191 | AllowsRange = false,
192 | AllowsOptional = true,
193 | AllowedComponents = 0
194 | },
195 | }
196 |
197 | return {
198 | Keywords = {
199 | map = true,
200 | set = true,
201 | type = true,
202 | enum = true,
203 | struct = true,
204 |
205 | event = true,
206 | ["function"] = true,
207 |
208 | scope = true,
209 | option = true,
210 |
211 | export = true,
212 | },
213 |
214 | Primtives = Primitives,
215 | GetCasing = function(Case: Case): Cases
216 | local Index = Indexes[Case]
217 | if not Index then
218 | error(`Unknown casing "{Case}", expectd one of "Pascal" or "Camel" or "Snake"`)
219 | end
220 |
221 | local Casing: any = {}
222 | for Key, Options in Cases do
223 | Casing[Key] = Options[Index]
224 | end
225 | return Casing
226 | end
227 | }
--------------------------------------------------------------------------------
/src/Templates/Base.luau:
--------------------------------------------------------------------------------
1 | return [[local Invocations = 0
2 |
3 | local SendSize = 64
4 | local SendOffset = 0
5 | local SendCursor = 0
6 | local SendBuffer = buffer.create(64)
7 | local SendInstances = {}
8 |
9 | local RecieveCursor = 0
10 | local RecieveBuffer = buffer.create(64)
11 |
12 | local RecieveInstances = {}
13 | local RecieveInstanceCursor = 0
14 |
15 | local Null = newproxy()
16 |
17 | type Entry = {
18 | value: any,
19 | next: Entry?
20 | }
21 |
22 | type Queue = {
23 | head: Entry?,
24 | tail: Entry?
25 | }
26 |
27 | type BufferSave = {
28 | Size: number,
29 | Cursor: number,
30 | Buffer: buffer,
31 | Instances: {Instance}
32 | }
33 |
34 | local function Read(Bytes: number)
35 | local Offset = RecieveCursor
36 | RecieveCursor += Bytes
37 | return Offset
38 | end
39 |
40 | local function Save(): BufferSave
41 | return {
42 | Size = SendSize,
43 | Cursor = SendCursor,
44 | Buffer = SendBuffer,
45 | Instances = SendInstances
46 | }
47 | end
48 |
49 | local function Load(Save: BufferSave?)
50 | if Save then
51 | SendSize = Save.Size
52 | SendCursor = Save.Cursor
53 | SendOffset = Save.Cursor
54 | SendBuffer = Save.Buffer
55 | SendInstances = Save.Instances
56 | return
57 | end
58 |
59 | SendSize = 64
60 | SendCursor = 0
61 | SendOffset = 0
62 | SendBuffer = buffer.create(64)
63 | SendInstances = {}
64 | end
65 |
66 | local function Invoke()
67 | if Invocations == 255 then
68 | Invocations = 0
69 | end
70 |
71 | local Invocation = Invocations
72 | Invocations += 1
73 | return Invocation
74 | end
75 |
76 | local function Allocate(Bytes: number)
77 | local InUse = (SendCursor + Bytes)
78 | if InUse > SendSize then
79 | --> Avoid resizing the buffer for every write
80 | while InUse > SendSize do
81 | SendSize *= 1.5
82 | end
83 |
84 | local Buffer = buffer.create(SendSize)
85 | buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
86 | SendBuffer = Buffer
87 | end
88 |
89 | SendOffset = SendCursor
90 | SendCursor += Bytes
91 |
92 | return SendOffset
93 | end
94 |
95 | local function CreateQueue(): Queue
96 | return {
97 | head = nil,
98 | tail = nil
99 | }
100 | end
101 |
102 | local function Pop(queue: Queue): any
103 | local head = queue.head
104 | if head == nil then
105 | return
106 | end
107 |
108 | queue.head = head.next
109 | return head.value
110 | end
111 |
112 | local function Push(queue: Queue, value: any)
113 | local entry: Entry = {
114 | value = value,
115 | next = nil
116 | }
117 |
118 | if queue.tail ~= nil then
119 | queue.tail.next = entry
120 | end
121 |
122 | queue.tail = entry
123 |
124 | if queue.head == nil then
125 | queue.head = entry
126 | end
127 | end
128 |
129 | local Calls = table.create(256)
130 |
131 | local Events: any = {
132 | Reliable = table.create(256),
133 | Unreliable = table.create(256)
134 | }
135 |
136 | local Queue: any = {
137 | Reliable = table.create(256),
138 | Unreliable = table.create(256)
139 | }
140 |
141 | ]]
--------------------------------------------------------------------------------
/src/Templates/Client.luau:
--------------------------------------------------------------------------------
1 | return [[local ReplicatedStorage = game:GetService("ReplicatedStorage")
2 | local RunService = game:GetService("RunService")
3 |
4 | -- SPLIT --
5 | if not RunService:IsClient() then
6 | error("Client network module can only be required from the client.")
7 | end
8 |
9 | local Reliable: RemoteEvent = ReplicatedStorage:WaitForChild(BASE_EVENT_NAME .. "_RELIABLE_REMOTE") :: RemoteEvent
10 | local Unreliable: UnreliableRemoteEvent = ReplicatedStorage:WaitForChild(BASE_EVENT_NAME .. "_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
11 |
12 | local function StepReplication()
13 | if SendCursor <= 0 then
14 | return
15 | end
16 |
17 | local Buffer = buffer.create(SendCursor)
18 | buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
19 | Reliable:FireServer(Buffer, SendInstances)
20 |
21 | SendSize = 64
22 | SendCursor = 0
23 | SendOffset = 0
24 | SendBuffer = buffer.create(64)
25 | table.clear(SendInstances)
26 | end
27 | ]]
--------------------------------------------------------------------------------
/src/Templates/Server.luau:
--------------------------------------------------------------------------------
1 | return [[local Players = game:GetService("Players")
2 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
3 | local RunService = game:GetService("RunService")
4 |
5 | -- SPLIT --
6 | if not RunService:IsServer() then
7 | error("Server network module can only be required from the server.")
8 | end
9 |
10 | local Reliable: RemoteEvent = ReplicatedStorage:FindFirstChild(BASE_EVENT_NAME .. "_RELIABLE_REMOTE") :: RemoteEvent
11 | if not Reliable then
12 | local RemoteEvent = Instance.new("RemoteEvent")
13 | RemoteEvent.Name = BASE_EVENT_NAME .. "_RELIABLE_REMOTE"
14 | RemoteEvent.Parent = ReplicatedStorage
15 | Reliable = RemoteEvent
16 | end
17 |
18 | local Unreliable: UnreliableRemoteEvent = ReplicatedStorage:FindFirstChild(BASE_EVENT_NAME .. "_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
19 | if not Unreliable then
20 | local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent")
21 | UnreliableRemoteEvent.Name = BASE_EVENT_NAME .. "_UNRELIABLE_REMOTE"
22 | UnreliableRemoteEvent.Parent = ReplicatedStorage
23 | Unreliable = UnreliableRemoteEvent
24 | end
25 |
26 | local PlayersMap: {[Player]: BufferSave} = {}
27 |
28 | Players.PlayerRemoving:Connect(function(Player)
29 | PlayersMap[Player] = nil
30 | end)
31 |
32 | local function StepReplication()
33 | for Player, Send in PlayersMap do
34 | if Send.Cursor <= 0 then
35 | continue
36 | end
37 |
38 | local Buffer = buffer.create(Send.Cursor)
39 | buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor)
40 | Reliable:FireClient(Player, Buffer, Send.Instances)
41 |
42 | Send.Size = 64
43 | Send.Cursor = 0
44 | Send.Buffer = buffer.create(64)
45 | table.clear(Send.Instances)
46 | end
47 | end
48 | ]]
--------------------------------------------------------------------------------
/test/Client.luau:
--------------------------------------------------------------------------------
1 | local Shared = require("Shared")
2 |
3 | local Signal = Shared.Signal
4 | local ClientEnviornment = Shared.GetEnvironment()
5 |
6 | local function Clone(Source: buffer): buffer
7 | local Size = buffer.len(Source)
8 | local Target = buffer.create(Size)
9 | buffer.copy(Target, 0, Source, 0, Size)
10 | return Target
11 | end
12 |
13 | local function Fire(Reliable: boolean, ...)
14 | local Arguments = {...}
15 | local Buffer = #Arguments == 2 and Arguments[1] or Arguments[2]
16 | local Instances = Arguments[#Arguments]
17 |
18 | Buffer = Clone(Buffer)
19 | Instances = table.clone(Instances)
20 |
21 | Shared.Bridge:Fire("Client", Reliable and "BLINK_RELIABLE_REMOTE" or "BLINK_UNRELIABLE_REMOTE", Buffer, Instances)
22 | end
23 |
24 | ClientEnviornment.Instances.BLINK_RELIABLE_REMOTE = {
25 | FireServer = function(self, ...)
26 | Fire(true, ...)
27 | end,
28 | OnClientEvent = Signal.new()
29 | }
30 |
31 | ClientEnviornment.Instances.BLINK_UNRELIABLE_REMOTE = {
32 | FireServer = function(self, ...)
33 | Fire(false, ...)
34 | end,
35 | OnClientEvent = Signal.new()
36 | }
37 |
38 | Shared.Bridge:Connect(function(From: string, Remote: string, Buffer: buffer, Instances: {Instance})
39 | if From == "Client" then
40 | return
41 | end
42 |
43 | for Index, Value in Instances do
44 | if Value == Shared.ServerOnlyInstance then
45 | Instances[Index] = nil
46 | end
47 | end
48 |
49 | ClientEnviornment.Instances[Remote].OnClientEvent:Fire(Buffer, Instances)
50 | end)
51 |
52 | return ClientEnviornment
--------------------------------------------------------------------------------
/test/Server.luau:
--------------------------------------------------------------------------------
1 | local Shared = require("Shared")
2 |
3 | local Signal = Shared.Signal
4 | local ServerEnviornment = Shared.GetEnvironment()
5 |
6 | local function Clone(Source: buffer): buffer
7 | local Size = buffer.len(Source)
8 | local Target = buffer.create(Size)
9 | buffer.copy(Target, 0, Source, 0, Size)
10 | return Target
11 | end
12 |
13 | local function Fire(Reliable: boolean, ...)
14 | local Arguments = {...}
15 | local Buffer = #Arguments == 2 and Arguments[1] or Arguments[2]
16 | local Instances = Arguments[#Arguments]
17 |
18 | Buffer = Clone(Buffer)
19 | Instances = table.clone(Instances)
20 |
21 | Shared.Bridge:Fire("Server", Reliable and "BLINK_RELIABLE_REMOTE" or "BLINK_UNRELIABLE_REMOTE", Buffer, Instances)
22 | end
23 |
24 | ServerEnviornment.Instances.BLINK_RELIABLE_REMOTE = {
25 | FireClient = function(self, Player, ...)
26 | Fire(true, ...)
27 | end,
28 | FireAllClients = function(self, ...)
29 | Fire(true, ...)
30 | end,
31 | OnServerEvent = Signal.new()
32 | }
33 |
34 | ServerEnviornment.Instances.BLINK_UNRELIABLE_REMOTE = {
35 | FireClient = function(Player, ...)
36 | Fire(false, ...)
37 | end,
38 | FireAllClients = function(self, ...)
39 | Fire(false, ...)
40 | end,
41 | OnServerEvent = Signal.new()
42 | }
43 |
44 | Shared.Bridge:Connect(function(From: string, Remote: string, Buffer: buffer, Instances: {Instance})
45 | if From == "Server" then
46 | return
47 | end
48 |
49 | ServerEnviornment.Instances[Remote].OnServerEvent:Fire(Shared.Player, Buffer, Instances)
50 | end)
51 |
52 | return ServerEnviornment
--------------------------------------------------------------------------------
/test/Shared.luau:
--------------------------------------------------------------------------------
1 | local task = require("@lune/task")
2 | local roblox = require("@lune/roblox")
3 |
4 | local Player = newproxy(true)
5 | local Metatable = getmetatable(Player)
6 | Metatable.__tostring = function()
7 | return "Player"
8 | end
9 |
10 | local ServerOnlyInstance = (newproxy() :: any) :: Instance
11 |
12 | local Signal = {}
13 | Signal.__index = Signal
14 |
15 | function Signal.new()
16 | return setmetatable({
17 | Connections = {}
18 | }, Signal)
19 | end
20 |
21 | function Signal:Fire(...)
22 | for _, Function in self.Connections do
23 | task.spawn(Function, ...)
24 | end
25 | end
26 |
27 | function Signal:Connect(Callback: (...unknown) -> (unknown))
28 | local Key = tostring(os.clock())
29 | self.Connections[Key] = Callback
30 | return {
31 | Disconnect = function()
32 | self.Connections[Key] = nil
33 | end
34 | }
35 | end
36 |
37 | --> Game emulation enviornment
38 | local function GetEnvironment()
39 | local Instances = {}
40 | local Services = {
41 | Players = {
42 | PlayerRemoving = Signal.new(),
43 | GetPlayers = function(self)
44 | return {Player}
45 | end
46 | },
47 |
48 | ReplicatedStorage = {
49 | Instances = Instances,
50 | FindFirstChild = function(self, Name: string)
51 | assert(Instances[Name], `Attempted to find child: "{Name}", but it was not defined.`)
52 | return Instances[Name]
53 | end
54 | },
55 |
56 | RunService = {
57 | Heartbeat = Signal.new(),
58 | IsClient = function(self)
59 | return true
60 | end,
61 | IsServer = function(self)
62 | return true
63 | end,
64 | IsRunning = function(self)
65 | return true
66 | end
67 | }
68 | }
69 |
70 | Services.ReplicatedStorage.WaitForChild = Services.ReplicatedStorage.FindFirstChild
71 |
72 | task.spawn(function()
73 | while true do
74 | local DeltaTime = task.wait(1/60)
75 | for Index, Function in Services.RunService.Heartbeat.Connections do
76 | Function(DeltaTime)
77 | end
78 | end
79 | end)
80 |
81 | local Color3 = {}
82 | function Color3.new(R: number, G: number, B: number): Color3
83 | return table.freeze({R = R, G = G, B = B, __typeof = "Color3"}) :: any
84 | end
85 |
86 | function Color3.fromRGB(R: number, G: number, B: number): Color3
87 | return Color3.new(R / 255, G / 255, B / 255)
88 | end
89 |
90 | local DateTime = {}
91 | function DateTime.fromUnixTimestamp(Timestamp: number): DateTime
92 | return table.freeze({UnixTimestamp = Timestamp, __typeof = "DateTime"})
93 | end
94 |
95 | function DateTime.fromUnixTimestampMillis(TimestampMillis: number): DateTime
96 | return table.freeze({UnixTimestampMillis = TimestampMillis, __typeof = "DateTime"})
97 | end
98 |
99 | local BrickColor = {}
100 | function BrickColor.new(Number: number): BrickColor
101 | return table.freeze({Number = Number, __typeof = "BrickColor"})
102 | end
103 |
104 | return {
105 | game = {
106 | GetService = function(self, Service: string)
107 | assert(Services[Service], `Attempted to get service: "{Service}", but it was not defined.`)
108 | return Services[Service]
109 | end
110 | },
111 |
112 | Color3 = Color3,
113 | DateTime = DateTime,
114 | Services = Services,
115 | Instances = Instances,
116 | BrickColor = BrickColor,
117 | }
118 | end
119 |
120 | return {
121 | Signal = Signal,
122 | Player = Player,
123 | Bridge = Signal.new(),
124 | GetEnvironment = GetEnvironment,
125 | ServerOnlyInstance = roblox.Instance.new("Instance"),
126 | }
127 |
128 |
--------------------------------------------------------------------------------
/test/Sources/Generics.blink:
--------------------------------------------------------------------------------
1 | option ClientOutput = "../../Network/ClientGenerics.luau"
2 | option ServerOutput = "../../Network/ServerGenerics.luau"
3 | option FutureLibrary = ""
4 | option PromiseLibrary = ""
5 |
6 | enum a = "Type" {
7 | Header {
8 | Sequence: u8,
9 | Fragments: u8
10 | },
11 | Fragment {
12 | Fragment: u8,
13 | Fragments: u8,
14 | }
15 | }
16 |
17 | map GenericMap = {[Key]: Value}
18 |
19 | enum GenericEnum = "Type" {
20 | A {
21 | Data: T
22 | },
23 | B {
24 | Data: G
25 | }
26 | }
27 |
28 | map Map = GenericMap
29 | enum b = GenericEnum
30 |
31 | struct Packet {
32 | Sequence: u16,
33 | Ack: u16,
34 | Data: enum "Type" {
35 | Single {
36 | Data: T
37 | },
38 | Fragment {
39 | Fragment: u8,
40 | Fragments: u8,
41 | Data: T
42 | }
43 | }
44 | }
45 |
46 | export struct Entity {
47 | id: u8,
48 | pos: vector,
49 | angle: u16,
50 | }
51 |
52 | event Snapshot {
53 | From: Client,
54 | Type: Unreliable,
55 | Call: SingleSync,
56 | Data: Packet
57 | }
--------------------------------------------------------------------------------
/test/Sources/Import.blink:
--------------------------------------------------------------------------------
1 | option ClientOutput = "../../Network/ClientImports.luau"
2 | option ServerOutput = "../../Network/ServerImports.luau"
3 |
4 | import "./Generics.blink"
5 | import "./Generics.blink" as Example
6 | import "./Sub-Sources/Source.blink"
7 | import "./Sub-Sources/Source2.blink"
--------------------------------------------------------------------------------
/test/Sources/Indexers.blink:
--------------------------------------------------------------------------------
1 | option ClientOutput = "../../Network/StringsClient.luau"
2 | option ServerOutput = "../../Network/StringsServer.luau"
3 |
4 | struct TestStruct {
5 | ["z z"]: u8,
6 | ["1 1"]: u8,
7 | ["2 2"]: u8,
8 | ["3 3"]: u8
9 | }
10 |
11 | set TestFlags = { "a a" }
12 | enum TestEnums = { "b b" }
13 |
14 | enum TestTaggedEnums = "Type" {
15 | ["c c"] {
16 | Test: u8
17 | }
18 | }
19 |
20 | event TestEvent
21 | {
22 | From: Server,
23 | Type: Unreliable,
24 | Call: SingleSync,
25 |
26 | Data: (TestStruct, TestFlags, TestEnums, TestTaggedEnums)
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/test/Sources/NoGenerics.blink:
--------------------------------------------------------------------------------
1 | option Casing = Pascal
2 | option Typescript = true
3 | option TypesOutput = "../../Network/Types.luau"
4 | option ClientOutput = "../../Network/Client.luau"
5 | option ServerOutput = "../../Network/Server.luau"
6 | option WriteValidations = true
7 | option ManualReplication = false
8 |
9 | import "./Sub-Sources/Source"
10 | import "./Sub-Sources/Source" as ImportA
11 |
12 | struct ImportReference {
13 | A: ImportA.a,
14 | B: Source.a
15 | }
16 |
17 | type A = CFrame
18 | type B = CFrame[10]
19 | type C = CFrame?
20 | type D = Color3
21 |
22 | export type Color = Color3
23 |
24 | export type ExactRange = f32(0)
25 | export type DecimalRange = f32(-5.5..10.5)
26 | export type NegativeRange = i8(-5..10)
27 | export type UnboundUpper = f32(-0.0..)
28 | export type UnboundLower = f32(..-0.0)
29 |
30 | type IntegerVector = vector
31 | type HalfPrecisionCFrame = CFrame
32 |
33 | type InstanceArray = Instance(Sound)[10]
34 | type InstanceArrayOptional = Instance(Sound)[10]?
35 | type ConstrainedArray = u8(0..10)[10]
36 |
37 | enum States = { A, B, C, D }
38 |
39 | set Flags8 = {F1, F2, F3, F4, F5, F6, F7, F8}
40 | set Flags16 = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16}
41 | set Flags32 = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32}
42 | set Flags33 = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32, F33}
43 |
44 | struct StructOfFlags {
45 | A: set {Flag, Flag1},
46 | B: set {Flag, Flag1},
47 | C: set {Flag, Flag1},
48 | }
49 |
50 | map MapSimple = {[string]: u8}
51 | map MapComplex = {[string(0..20)]: u8(0..100)[8]}
52 | map MapReference = {[States]: States}?
53 | export map MapNested = {[MapSimple]: MapSimple}
54 |
55 | map ArrayMap = {[u8]: u8}[0..20]
56 | enum ArrayEnum = {A, B, C}[0..20]
57 | struct ArrayStruct {
58 |
59 | }[0..20]
60 |
61 | type struct = u8
62 | struct Conflict {
63 | type: u8
64 | }
65 |
66 | enum Event = "Type" {
67 | Join {
68 | Name: string,
69 | UserId: f64,
70 | },
71 | Chat {
72 | UserId: f64,
73 | Message: string
74 | },
75 | Leave {
76 | UserId: f64
77 | }
78 | }
79 |
80 | type Float16 = f16
81 |
82 | export struct Standalone {
83 | One: u8,
84 | Two: u8,
85 | Three: u8,
86 | Event: Event,
87 | Nested: struct {
88 | Four: string,
89 | Five: string
90 | }
91 | }
92 |
93 | type Number = u8
94 | struct Example {
95 | Field: u8?,
96 | Float16Field: f16?,
97 | Enum: States,
98 | Nested: struct {
99 | Guh: u8,
100 | Array: u8[10],
101 | OptionalArray: u8[2]?
102 | }
103 | }
104 |
105 | set PrimitiveFlag = {
106 | CFrame
107 | }
108 |
109 | enum PrimitiveEnum = {
110 | CFrame
111 | }
112 |
113 | enum PrimitiveTagEnum = "Type" {
114 | CFrame {}
115 | }
116 |
117 | struct PrimitiveField {
118 | CFrame: u8
119 | }
120 |
121 | struct MergeStruct {
122 | ..Example,
123 | }
124 |
125 | struct MapStruct {
126 | Map: map {[string]: u8}
127 | }
128 |
129 | event ClientPollEvent {
130 | From: Client,
131 | Type: Reliable,
132 | Call: SingleSync,
133 | Poll: true,
134 | Data: MergeStruct
135 | }
136 |
137 | event ClientUnreliablePollEvent {
138 | From: Client,
139 | Type: Unreliable,
140 | Call: SingleSync,
141 | Poll: true,
142 | Data: MergeStruct
143 | }
144 |
145 | event ClientEmptyPollEvent {
146 | From: Client,
147 | Type: Reliable,
148 | Call: SingleSync,
149 | Poll: true,
150 | }
151 |
152 | event ClientEmptyUnreliablePollEvent {
153 | From: Client,
154 | Type: Unreliable,
155 | Call: SingleSync,
156 | Poll: true,
157 | }
158 |
159 | event ServerPollEvent {
160 | From: Server,
161 | Type: Reliable,
162 | Call: SingleSync,
163 | Poll: true,
164 | Data: MergeStruct
165 | }
166 |
167 | event ServerUnreliablePollEvent {
168 | From: Server,
169 | Type: Unreliable,
170 | Call: SingleSync,
171 | Poll: true,
172 | Data: MergeStruct
173 | }
174 |
175 | event ServerEmptyPollEvent {
176 | From: Server,
177 | Type: Reliable,
178 | Call: SingleSync,
179 | Poll: true,
180 | }
181 |
182 | event ServerEmptyUnreliablePollEvent {
183 | From: Server,
184 | Type: Unreliable,
185 | Call: SingleSync,
186 | Poll: true,
187 | }
188 |
189 | event ClientEmptyEvent {
190 | From: Client,
191 | Type: Reliable,
192 | Call: SingleSync
193 | }
194 |
195 | event ClientEmptyUnreliableEvent {
196 | From: Client,
197 | Type: Unreliable,
198 | Call: SingleSync
199 | }
200 |
201 | event EmptyEvent {
202 | From: Server,
203 | Type: Reliable,
204 | Call: SingleSync,
205 | }
206 |
207 | event EmptyUnreliableEvent {
208 | From: Server,
209 | Type: Unreliable,
210 | Call: SingleSync,
211 | }
212 |
213 | function EmptyFunction {
214 | Yield: Coroutine
215 | }
216 |
217 | event Flags8 {
218 | From: Server,
219 | Type: Reliable,
220 | Call: SingleSync,
221 | Data: Flags8
222 | }
223 |
224 | event Flags16 {
225 | From: Server,
226 | Type: Reliable,
227 | Call: SingleSync,
228 | Data: Flags16
229 | }
230 |
231 | event Flags32 {
232 | From: Server,
233 | Type: Reliable,
234 | Call: SingleSync,
235 | Data: Flags32
236 | }
237 | event Flags33 {
238 | From: Server,
239 | Type: Reliable,
240 | Call: SingleSync,
241 | Data: Flags33
242 | }
243 |
244 | event DynamicMap {
245 | From: Server,
246 | Type: Reliable,
247 | Call: SingleSync,
248 | Data: map {[u8]: struct {Static: u8, Dynamic: buffer}}
249 | }
250 |
251 | event DynamicArray {
252 | From: Server,
253 | Type: Reliable,
254 | Call: SingleSync,
255 | Data: struct {Static: u8, Dynamic: buffer}[12]
256 | }
257 |
258 | event PlayerEvent {
259 | From: Server,
260 | Type: Reliable,
261 | Call: SingleSync,
262 | Data: Event
263 | }
264 |
265 | event Booleans {
266 | from: Client,
267 | type: Reliable,
268 | call: SingleAsync,
269 | data: boolean[]
270 | }
271 |
272 | event Unknown {
273 | Type: Reliable,
274 | From: Server,
275 | Call: SingleSync,
276 | Data: unknown,
277 | }
278 |
279 | event MapEvent {
280 | From: Server,
281 | Type: Reliable,
282 | Call: SingleSync,
283 | Data: MapSimple
284 | }
285 |
286 | event MapStructEvent {
287 | From: Server,
288 | Type: Reliable,
289 | Call: SingleSync,
290 | Data: MapStruct
291 | }
292 |
293 | event MapComplexEvent {
294 | From: Server,
295 | Type: Reliable,
296 | Call: SingleSync,
297 | Data: MapComplex
298 | }
299 |
300 | event MapReferenceEvent {
301 | From: Server,
302 | Type: Reliable,
303 | Call: SingleSync,
304 | Data: MapReference
305 | }
306 |
307 | event ArrayPrimitive {
308 | From: Server,
309 | Type: Unreliable,
310 | Call: SingleSync,
311 | Data: u8[8]
312 | }
313 |
314 | event OptionalPrimitive {
315 | From: Server,
316 | Type: Unreliable,
317 | Call: SingleSync,
318 | Data: u8?
319 | }
320 |
321 | event ManyReliableSync {
322 | From: Server,
323 | Type: Reliable,
324 | Call: ManySync,
325 | Data: u8
326 | }
327 |
328 | event ManyReliableAsync {
329 | From: Server,
330 | Type: Reliable,
331 | Call: ManyAsync,
332 | Data: u8
333 | }
334 |
335 | event ManyUnreliableSync {
336 | From: Server,
337 | Type: Unreliable,
338 | Call: ManySync,
339 | Data: u8
340 | }
341 |
342 | event ManyUnreliableAsync {
343 | From: Server,
344 | Type: Unreliable,
345 | Call: ManyAsync,
346 | Data: u8
347 | }
348 |
349 | event ReliableServer {
350 | From: Server,
351 | Type: Reliable,
352 | Call: SingleSync,
353 | Data: u8
354 | }
355 |
356 | event ReliableServerAsync {
357 | From: Server,
358 | Type: Reliable,
359 | Call: SingleAsync,
360 | Data: u8
361 | }
362 |
363 | event ReliableClient {
364 | From: Client,
365 | Type: Reliable,
366 | Call: SingleSync,
367 | Data: u8
368 | }
369 |
370 | event UnreliableServer {
371 | From: Server,
372 | Type: Unreliable,
373 | Call: SingleSync,
374 | Data: u8
375 | }
376 |
377 | event UnreliableServerAsync {
378 | From: Server,
379 | Type: Unreliable,
380 | Call: SingleAsync,
381 | Data: u8
382 | }
383 |
384 | event UnreliableClient {
385 | From: Client,
386 | Type: Unreliable,
387 | Call: SingleSync,
388 | Data: u8
389 | }
390 |
391 | event InstanceAny {
392 | From: Server,
393 | Type: Reliable,
394 | Call: SingleSync,
395 | Data: Instance
396 | }
397 |
398 | event InstanceOfType {
399 | From: Server,
400 | Type: Reliable,
401 | Call: SingleSync,
402 | Data: Instance(Sound)
403 | }
404 |
405 | event InstanceOptional {
406 | From: Server,
407 | Type: Reliable,
408 | Call: SingleSync,
409 | Data: Instance?
410 | }
411 |
412 | event Reference {
413 | From: Server,
414 | Type: Reliable,
415 | Call: SingleSync,
416 | Data: Example?
417 | }
418 |
419 | event ReferenceArray {
420 | From: Server,
421 | Type: Reliable,
422 | Call: SingleSync,
423 | Data: Example[8]
424 | }
425 |
426 | event ReferenceOptional {
427 | From: Server,
428 | Type: Reliable,
429 | Call: SingleSync,
430 | Data: Example?
431 | }
432 |
433 | function RemoteFunction {
434 | Yield: Coroutine,
435 | Data: u8,
436 | Return: u8
437 | }
438 |
439 | event Tuple {
440 | From: Server,
441 | Type: Reliable,
442 | Call: SingleSync,
443 | Data: (u8, States, u16?, Instance, Instance?, u8[8])
444 | }
445 |
446 | event TupleUnreliable {
447 | From: Server,
448 | Type: Unreliable,
449 | Call: SingleSync,
450 | Data: (u8, States, u16?, Instance, Instance?)
451 | }
452 |
453 | function TupleFunction {
454 | Yield: Coroutine,
455 | Data: (u8, States, u16?, Instance, Instance?),
456 | Return: (Instance?, Instance, u16?, States, u8)
457 | }
458 |
459 | scope AnotherScope {
460 | event InScopeEvent {
461 | From: Server,
462 | Type: Reliable,
463 | Call: SingleSync,
464 | Data: u8
465 | }
466 |
467 | scope ScopeWithinAnotherScope {
468 | type ExampleType = u8
469 | event InAnotherScopeEvent {
470 | From: Server,
471 | Type: Reliable,
472 | Call: SingleSync,
473 | Data: ExampleType
474 | }
475 | }
476 | }
477 |
478 | scope ExampleScope {
479 | event InScopeEvent {
480 | From: Server,
481 | Type: Reliable,
482 | Call: SingleSync,
483 | Data: AnotherScope.ScopeWithinAnotherScope.ExampleType
484 | }
485 | }
--------------------------------------------------------------------------------
/test/Sources/Polling.blink:
--------------------------------------------------------------------------------
1 | option Casing = Snake
2 | option UsePolling = true
3 | option ClientOutput = "../../Network/PollingClient.luau"
4 | option ServerOutput = "../../Network/PollingServer.luau"
5 |
6 | type number = u8
7 |
8 | struct a {
9 | foo: u8
10 | }
11 |
12 | struct b {
13 | bar: u8,
14 | }
15 |
16 | struct c {
17 | a: A,
18 | B: b,
19 | C: C
20 | }
21 |
22 | struct d {
23 | ..a,
24 | ..b,
25 | ..c,
26 | }
27 |
28 | event PollingEvent {
29 | From: Client,
30 | Type: Reliable,
31 | Call: SingleSync,
32 | Poll: true
33 | }
34 |
35 | event ImplicitPollingEvent {
36 | From: Client,
37 | Type: Reliable,
38 | Call: SingleSync,
39 | Poll: true
40 | }
--------------------------------------------------------------------------------
/test/Sources/Scope.blink:
--------------------------------------------------------------------------------
1 | option RemoteScope = "Package"
2 | option ClientOutput = "../../Network/ClientPackage.luau"
3 | option ServerOutput = "../../Network/ServerPackage.luau"
--------------------------------------------------------------------------------
/test/Sources/Sub-Sources/Source.blink:
--------------------------------------------------------------------------------
1 | import "./Source2.txt"
2 |
3 | type a = u8
--------------------------------------------------------------------------------
/test/Sources/Sub-Sources/Source2.blink:
--------------------------------------------------------------------------------
1 | type a = u8
--------------------------------------------------------------------------------
/test/Sources/Test.blink:
--------------------------------------------------------------------------------
1 | option Casing = Pascal
2 | option Typescript = true
3 | option TypesOutput = "../../Network/Types.luau"
4 | option ClientOutput = "../../Network/Client.luau"
5 | option ServerOutput = "../../Network/Server.luau"
6 | option SyncValidation = true
7 | option WriteValidations = true
8 | option ManualReplication = false
9 |
10 | import "./Indexers"
11 | import "./Sub-Sources/Source"
12 | import "./Sub-Sources/Source" as ImportA
13 |
14 | struct ImportReference {
15 | A: ImportA.a,
16 | B: Source.a
17 | }
18 |
19 | type A = CFrame
20 | type B = CFrame[..10]
21 | type C = CFrame?
22 | type D = Color3
23 |
24 | export type Byte = u8
25 |
26 | export type Color = Color3
27 | export type ColorArray = Color3[]
28 | export type DateSeconds = DateTime
29 | export type DateMilliseconds = DateTimeMillis
30 | export type BrickkColor = BrickColor
31 |
32 | export type ExactRange = f32(0)
33 | export type DecimalRange = f32(-5.5..10.5)
34 | export type NegativeRange = i8(-5..10)
35 | export type UnboundUpper = f32(-0.0..)
36 | export type UnboundLower = f32(..-0.0)
37 |
38 | export type Buffer = buffer
39 | export type ExactBuffer = buffer(9)
40 | export type BoundedBuffer = buffer(0..1000)
41 |
42 | type IntegerVector = vector
43 | type HalfPrecisionCFrame = CFrame
44 |
45 | type InstanceArray = Instance(Sound)[10]
46 | type InstanceArrayOptional = Instance(Sound)[10]?
47 | type ConstrainedArray = u8(0..10)[10]
48 |
49 | export type MultiDimensionalArray = u8[][][]
50 |
51 | enum States = { A, B, C, D }
52 |
53 | set Flags8 = {F1, F2, F3, F4, F5, F6, F7, F8}
54 | set Flags16 = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16}
55 | set Flags32 = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32}
56 | set Flags33 = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32, F33}
57 |
58 | struct StructOfFlags {
59 | A: set {Flag, Flag1},
60 | B: set {Flag, Flag1},
61 | C: set {Flag, Flag1},
62 | }
63 |
64 | map MapSimple = {[string]: u8}
65 | map MapComplex = {[string(0..20)]: u8(0..100)[8]}
66 | map MapReference = {[States]: States}?
67 | export map MapNested = {[MapSimple]: MapSimple}
68 |
69 | map ArrayMap = {[u8]: u8}[0..20]
70 | enum ArrayEnum = {A, B, C}[0..20]
71 | struct ArrayStruct {
72 |
73 | }[0..20]
74 |
75 | struct Generic {
76 | Data: A,
77 | Array: A[],
78 | Optional: C?,
79 | Nested: struct {
80 | Value: B
81 | }
82 | }
83 |
84 | type struct = u8
85 | struct Conflict {
86 | type: u8
87 | }
88 |
89 | enum Event = "Type" {
90 | Join {
91 | Name: string,
92 | UserId: f64,
93 | },
94 | Chat {
95 | UserId: f64,
96 | Message: string
97 | },
98 | Leave {
99 | UserId: f64
100 | }
101 | }
102 |
103 | type Float16 = f16
104 |
105 | export struct Standalone {
106 | One: u8,
107 | Two: u8,
108 | Three: u8,
109 | Event: Event,
110 | Nested: struct {
111 | Four: string,
112 | Five: string
113 | }
114 | }
115 |
116 | type Number = u8
117 | struct Example {
118 | Field: u8?,
119 | Float16Field: f16?,
120 | Enum: States,
121 | Nested: struct {
122 | Guh: u8,
123 | Array: u8[10],
124 | OptionalArray: u8[2]?
125 | }
126 | }
127 |
128 | set PrimitiveFlag = {
129 | CFrame
130 | }
131 |
132 | enum PrimitiveEnum = {
133 | CFrame
134 | }
135 |
136 | enum PrimitiveTagEnum = "Type" {
137 | CFrame {}
138 | }
139 |
140 | struct PrimitiveField {
141 | CFrame: u8
142 | }
143 |
144 | map GenericMap = {[K]: V}
145 |
146 | struct GenericMerge {
147 | Map: GenericMap,
148 | Data: A,
149 | Struct: Generic,
150 | AnotherNested: struct {
151 | Value: B
152 | }
153 | }
154 |
155 | struct MergeStruct {
156 | ..Example,
157 | ..GenericMerge
158 | }
159 |
160 | struct MapStruct {
161 | Map: map {[string]: u8}
162 | }
163 |
164 | event SendOccupiedCell {
165 | From: Server,
166 | Type: Reliable,
167 | Call: SingleSync,
168 | Data: IntegerVector
169 | }
170 |
171 | event ClientPollEvent {
172 | From: Client,
173 | Type: Reliable,
174 | Call: Polling,
175 | Data: MergeStruct
176 | }
177 |
178 | event ClientUnreliablePollEvent {
179 | From: Client,
180 | Type: Unreliable,
181 | Call: Polling,
182 | Data: MergeStruct
183 | }
184 |
185 | event ClientEmptyPollEvent {
186 | From: Client,
187 | Type: Reliable,
188 | Call: Polling,
189 | }
190 |
191 | event ClientEmptyUnreliablePollEvent {
192 | From: Client,
193 | Type: Unreliable,
194 | Call: Polling,
195 | }
196 |
197 | event ServerPollEvent {
198 | From: Server,
199 | Type: Reliable,
200 | Call: Polling,
201 | Data: MergeStruct
202 | }
203 |
204 | event ServerUnreliablePollEvent {
205 | From: Server,
206 | Type: Unreliable,
207 | Call: Polling,
208 | Data: MergeStruct
209 | }
210 |
211 | event ServerEmptyPollEvent {
212 | From: Server,
213 | Type: Reliable,
214 | Call: Polling,
215 | }
216 |
217 | event ServerEmptyUnreliablePollEvent {
218 | From: Server,
219 | Type: Unreliable,
220 | Call: SingleSync,
221 | Poll: true,
222 | }
223 |
224 | event ClientEmptyEvent {
225 | From: Client,
226 | Type: Reliable,
227 | Call: SingleSync
228 | }
229 |
230 | event ClientEmptyUnreliableEvent {
231 | From: Client,
232 | Type: Unreliable,
233 | Call: SingleSync
234 | }
235 |
236 | event EmptyEvent {
237 | From: Server,
238 | Type: Reliable,
239 | Call: SingleSync,
240 | }
241 |
242 | event EmptyUnreliableEvent {
243 | From: Server,
244 | Type: Unreliable,
245 | Call: SingleSync,
246 | }
247 |
248 | function EmptyFunction {
249 | Yield: Coroutine
250 | }
251 |
252 | event Flags8 {
253 | From: Server,
254 | Type: Reliable,
255 | Call: SingleSync,
256 | Data: Flags8
257 | }
258 |
259 | event Flags16 {
260 | From: Server,
261 | Type: Reliable,
262 | Call: SingleSync,
263 | Data: Flags16
264 | }
265 |
266 | event Flags32 {
267 | From: Server,
268 | Type: Reliable,
269 | Call: SingleSync,
270 | Data: Flags32
271 | }
272 | event Flags33 {
273 | From: Server,
274 | Type: Reliable,
275 | Call: SingleSync,
276 | Data: Flags33
277 | }
278 |
279 | event DynamicMap {
280 | From: Server,
281 | Type: Reliable,
282 | Call: SingleSync,
283 | Data: map {[u8]: struct {Static: u8, Dynamic: buffer}}
284 | }
285 |
286 | event DynamicArray {
287 | From: Server,
288 | Type: Reliable,
289 | Call: SingleSync,
290 | Data: struct {Static: u8, Dynamic: buffer}[12]
291 | }
292 |
293 | event PlayerEvent {
294 | From: Server,
295 | Type: Reliable,
296 | Call: SingleSync,
297 | Data: Event
298 | }
299 |
300 | event Booleans {
301 | from: Client,
302 | type: Reliable,
303 | call: SingleAsync,
304 | data: boolean[]
305 | }
306 |
307 | event Generic {
308 | From: Server,
309 | Type: Reliable,
310 | Data: Generic,
311 | Call: SingleSync
312 | }
313 |
314 | event GenericArray {
315 | From: Server,
316 | Type: Reliable,
317 | Data: Generic[]?,
318 | Call: SingleSync
319 | }
320 |
321 | event Unknown {
322 | Type: Reliable,
323 | From: Server,
324 | Call: SingleSync,
325 | Data: unknown,
326 | }
327 |
328 | event MapEvent {
329 | From: Server,
330 | Type: Reliable,
331 | Call: SingleSync,
332 | Data: MapSimple
333 | }
334 |
335 | event MapStructEvent {
336 | From: Server,
337 | Type: Reliable,
338 | Call: SingleSync,
339 | Data: MapStruct
340 | }
341 |
342 | event MapComplexEvent {
343 | From: Server,
344 | Type: Reliable,
345 | Call: SingleSync,
346 | Data: MapComplex
347 | }
348 |
349 | event MapReferenceEvent {
350 | From: Server,
351 | Type: Reliable,
352 | Call: SingleSync,
353 | Data: MapReference
354 | }
355 |
356 | event ArrayPrimitive {
357 | From: Server,
358 | Type: Unreliable,
359 | Call: SingleSync,
360 | Data: u8[8]
361 | }
362 |
363 | event OptionalPrimitive {
364 | From: Server,
365 | Type: Unreliable,
366 | Call: SingleSync,
367 | Data: u8?
368 | }
369 |
370 | event ManyReliableSync {
371 | From: Server,
372 | Type: Reliable,
373 | Call: ManySync,
374 | Data: u8
375 | }
376 |
377 | event ManyReliableAsync {
378 | From: Server,
379 | Type: Reliable,
380 | Call: ManyAsync,
381 | Data: u8
382 | }
383 |
384 | event ManyUnreliableSync {
385 | From: Server,
386 | Type: Unreliable,
387 | Call: ManySync,
388 | Data: u8
389 | }
390 |
391 | event ManyUnreliableAsync {
392 | From: Server,
393 | Type: Unreliable,
394 | Call: ManyAsync,
395 | Data: u8
396 | }
397 |
398 | event ReliableServer {
399 | From: Server,
400 | Type: Reliable,
401 | Call: SingleSync,
402 | Data: u8
403 | }
404 |
405 | event ReliableServerAsync {
406 | From: Server,
407 | Type: Reliable,
408 | Call: SingleAsync,
409 | Data: u8
410 | }
411 |
412 | event ReliableClient {
413 | From: Client,
414 | Type: Reliable,
415 | Call: SingleSync,
416 | Data: u8
417 | }
418 |
419 | event UnreliableServer {
420 | From: Server,
421 | Type: Unreliable,
422 | Call: SingleSync,
423 | Data: u8
424 | }
425 |
426 | event UnreliableServerAsync {
427 | From: Server,
428 | Type: Unreliable,
429 | Call: SingleAsync,
430 | Data: u8
431 | }
432 |
433 | event UnreliableClient {
434 | From: Client,
435 | Type: Unreliable,
436 | Call: SingleSync,
437 | Data: u8
438 | }
439 |
440 | event InstanceAny {
441 | From: Server,
442 | Type: Reliable,
443 | Call: SingleSync,
444 | Data: Instance
445 | }
446 |
447 | event InstanceOfType {
448 | From: Server,
449 | Type: Reliable,
450 | Call: SingleSync,
451 | Data: Instance(Sound)
452 | }
453 |
454 | event InstanceOptional {
455 | From: Server,
456 | Type: Reliable,
457 | Call: SingleSync,
458 | Data: Instance?
459 | }
460 |
461 | event Reference {
462 | From: Server,
463 | Type: Reliable,
464 | Call: SingleSync,
465 | Data: Example?
466 | }
467 |
468 | event ReferenceArray {
469 | From: Server,
470 | Type: Reliable,
471 | Call: SingleSync,
472 | Data: Example[8]
473 | }
474 |
475 | event ReferenceOptional {
476 | From: Server,
477 | Type: Reliable,
478 | Call: SingleSync,
479 | Data: Example?
480 | }
481 |
482 | function RemoteFunction {
483 | Yield: Coroutine,
484 | Data: u8,
485 | Return: u8
486 | }
487 |
488 | event Tuple {
489 | From: Server,
490 | Type: Reliable,
491 | Call: SingleSync,
492 | Data: (u8, States, u16?, Instance, Instance?, u8[8])
493 | }
494 |
495 | event TupleUnreliable {
496 | From: Server,
497 | Type: Unreliable,
498 | Call: SingleSync,
499 | Data: (u8, States, u16?, Instance, Instance?)
500 | }
501 |
502 | function TupleFunction {
503 | Yield: Coroutine,
504 | Data: (u8, States, u16?, Instance, Instance?),
505 | Return: (Instance?, Instance, u16?, States, u8)
506 | }
507 |
508 | scope AnotherScope {
509 | event InScopeEvent {
510 | From: Server,
511 | Type: Reliable,
512 | Call: SingleSync,
513 | Data: u8
514 | }
515 |
516 | scope ScopeWithinAnotherScope {
517 | type ExampleType = u8
518 | event InAnotherScopeEvent {
519 | From: Server,
520 | Type: Reliable,
521 | Call: SingleSync,
522 | Data: ExampleType
523 | }
524 | }
525 | }
526 |
527 | scope ExampleScope {
528 | event InScopeEvent {
529 | From: Server,
530 | Type: Reliable,
531 | Call: SingleSync,
532 | Data: AnotherScope.ScopeWithinAnotherScope.ExampleType
533 | }
534 | }
535 |
536 | scope Issue33 {
537 | enum Foo = {
538 | One,
539 | Two
540 | }
541 |
542 | map Map = {[Foo]: f64}
543 |
544 | enum Enum = "Tag" {
545 | Variant {
546 | field: Foo
547 | }
548 | }
549 |
550 | struct Struct {
551 | field: Foo
552 | }
553 |
554 | event Event {
555 | From: Server,
556 | Type: Reliable,
557 | Call: SingleSync,
558 | Data: (Map, Enum, Struct)
559 | }
560 | }
--------------------------------------------------------------------------------