├── .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 | [![GitHub License](https://img.shields.io/github/license/1Axen/blink?style=flat-square&color=%23a350af)](LICENSE) 6 | [![GitHub Release](https://img.shields.io/github/v/release/1Axen/blink?style=flat-square&color=%23a350af)](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 | [![](.github/screenshot.png)](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/button)](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 | ![Locate](/plugin/Locate.png) 13 | 14 | ## Side Menu 15 | You can open the side menu using the sandwich button on the left hand side. 16 | ![Menu](/plugin/Menu.png) 17 | 18 | Within the side menu you can manage your network description files. 19 | ![Navigation](/plugin/Navigation.png) 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 | ![Save](/plugin/Save.png) 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 | ![Generate](/plugin/Generate.png) 40 | 41 | Once you've selected your desired output destination simply press "Generate" and your files will be ready. 42 | 43 | ![GenerateSelected](/plugin/GenerateSelected.png) 44 | 45 | If no issues arise Blink will generate the following Folder containing your networking modules: 46 | 47 | ![Output](/plugin/Output.png) 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 | ![Editor](/plugin/Editor.png) 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 | ![Error](/plugin/Error.png) -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------