]>;
50 |
51 | /** Creates an object from a set of entries */
52 | fromEntries(
53 | this: void,
54 | i: ReadonlyArray
,
55 | ): Reconstruct<
56 | UnionToIntersection<
57 | P extends unknown
58 | ? {
59 | [k in P[0]]: P[1];
60 | }
61 | : never
62 | >
63 | >;
64 |
65 | /**
66 | * Returns true if empty, otherwise false.
67 | */
68 | isEmpty(this: void, o: object): boolean;
69 |
70 | /**
71 | * Returns a shallow copy of the object
72 | */
73 | copy(this: void, o: T): T;
74 |
75 | /**
76 | * Returns a deep copy of the object
77 | */
78 | deepCopy(this: void, o: T): T;
79 |
80 | /**
81 | * Returns true if
82 | * - each member of `a` equals each member of `b`
83 | * - `b` has no members that do not exist in `a`.
84 | *
85 | * Searches recursively.
86 | */
87 | deepEquals(this: void, a: object, b: object): boolean;
88 | }
89 |
90 | declare const Object: ObjectConstructor;
91 |
92 | export = Object;
93 |
--------------------------------------------------------------------------------
/src/utils/fetch-github-release/types.ts:
--------------------------------------------------------------------------------
1 | /** Information about the release being downloaded. */
2 | export interface FetchInfo {
3 | /** The folder the release was saved to. */
4 | readonly location: string;
5 |
6 | /** The owner of the repository. */
7 | readonly owner: string;
8 |
9 | /** The name of the repository. */
10 | readonly repo: string;
11 |
12 | /** The version of the release. */
13 | readonly tag: string;
14 |
15 | /** The specific asset that was downloaded. */
16 | readonly asset: "Source code" | string;
17 |
18 | /** Whether the operation downloaded a new release. */
19 | readonly updated: boolean;
20 | }
21 |
22 | export interface Author {
23 | readonly login: string;
24 | readonly id: number;
25 | readonly node_id: string;
26 | /** @example `https://avatars.githubusercontent.com/u/${id}?v=4` */
27 | readonly avatar_url: string;
28 | readonly gravatar_id: string;
29 | /** @example `https://api.github.com/users/${user}` */
30 | readonly url: string;
31 | /** @example `https://github.com/${user}` */
32 | readonly html_url: string;
33 | /** @example `https://api.github.com/users/${user}/followers` */
34 | readonly followers_url: string;
35 | /** @example `https://api.github.com/users/${user}/following{/other_user}` */
36 | readonly following_url: string;
37 | /** @example `https://api.github.com/users/${user}/gists{/gist_id}` */
38 | readonly gists_url: string;
39 | /** @example `https://api.github.com/users/${user}/starred{/owner}{/repo}` */
40 | readonly starred_url: string;
41 | /** @example `https://api.github.com/users/${user}/subscriptions` */
42 | readonly subscriptions_url: string;
43 | /** @example `https://api.github.com/users/${user}/orgs` */
44 | readonly organizations_url: string;
45 | /** @example `https://api.github.com/users/${user}/repos` */
46 | readonly repos_url: string;
47 | /** @example `https://api.github.com/users/${user}/events{/privacy}` */
48 | readonly events_url: string;
49 | /** @example `https://api.github.com/users/${user}/received_events` */
50 | readonly received_events_url: string;
51 | readonly type: string;
52 | readonly site_admin: boolean;
53 | }
54 |
55 | export interface Asset {
56 | /** @example `https://api.github.com/repos/${user}/${repo}/releases/assets/${id}` */
57 | readonly url: string;
58 | readonly id: number;
59 | readonly node_id: string;
60 | readonly name: string;
61 | readonly label?: string;
62 | readonly uploader: Author;
63 | readonly content_type: string;
64 | readonly state: string;
65 | readonly size: number;
66 | readonly download_count: number;
67 | readonly created_at: string;
68 | readonly updated_at: string;
69 | /** @example `https://github.com/${user}/${repo}/releases/download/${tag}/%{asset}` */
70 | readonly browser_download_url: string;
71 | }
72 |
73 | /**
74 | * Information about the latest release of a given Github repository.
75 | * See this [example](https://api.github.com/repos/Roblox/roact/releases/latest).
76 | */
77 | export interface Release {
78 | /** @example `https://api.github.com/repos/${user}/${repo}/releases/${id}` */
79 | readonly url: string;
80 | /** @example `https://api.github.com/repos/${user}/${repo}/releases/${id}/assets` */
81 | readonly assets_url: string;
82 | /** @example `https://uploads.github.com/repos/${user}/${repo}/releases/${id}/assets{?name,label}` */
83 | readonly upload_url: string;
84 | /** @example `https://github.com/${user}/${repo}/releases/tag/${latestVersion}` */
85 | readonly html_url: string;
86 | readonly id: number;
87 | readonly author: Author;
88 | readonly node_id: string;
89 | readonly tag_name: string;
90 | readonly target_commitish: string;
91 | readonly name: string;
92 | readonly draft: boolean;
93 | readonly prerelease: boolean;
94 | readonly created_at: string;
95 | readonly published_at: string;
96 | readonly assets: Asset[];
97 | /** @example `https://api.github.com/repos/${user}/${repo}/tarball/${tag}` */
98 | readonly tarball_url: string;
99 | /** @example `https://api.github.com/repos/${user}/${repo}/zipball/${tag}` */
100 | readonly zipball_url: string;
101 | readonly body: string;
102 | }
103 |
--------------------------------------------------------------------------------
/src/Package.ts:
--------------------------------------------------------------------------------
1 | import { Session, VirtualScript } from "core";
2 | import type { Executor } from "core";
3 | import type { FetchInfo } from "utils/fetch-github-release";
4 | import { pathUtils } from "utils/file-utils";
5 | import Make from "modules/make";
6 |
7 | /** Transforms files into Roblox objects and handles runtime. Acts as a wrapper for Session. */
8 | export class Package {
9 | /** A Folder containing objects created from the `build()` method. */
10 | public readonly tree = Make("Folder", { Name: "Tree" });
11 |
12 | /** The root directory of the project. */
13 | public readonly root: string;
14 |
15 | /** Information about the last call of `fetch` or `fetchLatest`. */
16 | public readonly fetchInfo?: FetchInfo;
17 |
18 | /** The Session for this project. */
19 | private readonly session: Session;
20 |
21 | /**
22 | * Create a new Rostruct Package.
23 | * @param root A path to the project directory.
24 | * @param fetchInfo Information about the downloaded GitHub release.
25 | */
26 | constructor(root: string, fetchInfo?: FetchInfo) {
27 | assert(type(root) === "string", "(Package) The path must be a string");
28 | assert(isfolder(root), `(Package) The path '${root}' must be a valid directory`);
29 | this.root = pathUtils.formatPath(root);
30 | this.session = new Session(root);
31 | this.fetchInfo = fetchInfo;
32 | }
33 |
34 | /**
35 | * Create a new Package from a downloaded GitHub release.
36 | * @param root A path to the project directory.
37 | * @param fetchInfo Information about the downloaded GitHub release.
38 | */
39 | public static readonly fromFetch = (fetchInfo: FetchInfo): Package => new Package(fetchInfo.location, fetchInfo);
40 |
41 | /**
42 | * Turns a folder in the root directory and all descendants into Roblox objects.
43 | * If `dir` is not provided, this function transforms the root directory.
44 | * @param fileOrFolder Optional specific folder to build in the root directory.
45 | * @param props Optional properties to set after building the folder.
46 | * @returns The Instance created.
47 | */
48 | public build(fileOrFolder = "", props?: { [property: string]: unknown }): Instance {
49 | assert(
50 | isfile(this.root + fileOrFolder) || isfolder(this.root + fileOrFolder),
51 | `(Package.build) The path '${this.root + fileOrFolder}' must be a file or folder`,
52 | );
53 |
54 | const instance = this.session.build(fileOrFolder);
55 | assert(instance, `(Package.build) The path '${this.root + fileOrFolder}' could not be turned into an Instance`);
56 |
57 | // Set object properties
58 | if (props !== undefined) {
59 | for (const [property, value] of pairs(props as unknown as Map)) {
60 | instance[property] = value;
61 | }
62 | }
63 |
64 | instance.Parent = this.tree;
65 |
66 | return instance;
67 | }
68 |
69 | /**
70 | * Simulate script runtime by running LocalScripts on deferred threads.
71 | * @returns
72 | * A promise that resolves with an array of scripts that finished executing.
73 | * If one script throws an error, the entire promise will cancel.
74 | */
75 | public start(): Promise {
76 | return this.session.simulate();
77 | }
78 |
79 | /**
80 | * Requires the given ModuleScript. If `module` is not provided, it requires
81 | * the first Instance in `tree`.
82 | * @param module The module to require.
83 | * @returns A promise that resolves with what the module returned.
84 | */
85 | public async require(module: ModuleScript): Promise> {
86 | assert(classIs(module, "ModuleScript"), `(Package.require) '${module}' must be a module`);
87 | assert(module.IsDescendantOf(this.tree), `(Package.require) '${module}' must be a descendant of Package.tree`);
88 | return VirtualScript.requireFromInstance(module);
89 | }
90 |
91 | /**
92 | * Requires the given ModuleScript. If `module` is not provided, it requires
93 | * the first Instance in `tree`.
94 | * @param module The module to require.
95 | * @returns What the module returned.
96 | */
97 | public requireAsync(module: ModuleScript): ReturnType {
98 | return this.require(module).expect();
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/typings/Engine.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the Roblox global environment.
3 | * @returns Roblox's global environment.
4 | */
5 | declare function getrenv(): { [key: string]: unknown };
6 |
7 | /**
8 | * Returns the current environment in use by the exploit.
9 | * @returns The exploit's global environment.
10 | * */
11 | declare function getgenv(): { [key: string]: unknown };
12 |
13 | /**
14 | * Loads a chunk.
15 | * If there are no syntactic errors, returns the compiled chunk as a function; otherwise, returns nil plus the error message.
16 | *
17 | * `chunkname` is used as the name of the chunk for error messages and debug information.
18 | * When absent, it defaults to chunk, if chunk is a string, or to "=(load)" otherwise.
19 | * @param chunk The string to load.
20 | * @param chunkname The name of the chunk.
21 | */
22 | declare function loadstring(
23 | chunk: string,
24 | chunkname?: string,
25 | ): LuaTuple<[(...params: Array) => unknown, string | undefined]>;
26 |
27 | /** Returns the name of the executor. */
28 | declare function identifyexecutor(): string;
29 |
30 | // Filesystem
31 |
32 | /** Check whether the given path points to a file. */
33 | declare function isfile(path: string): boolean;
34 |
35 | /** Check whether the given path points to a directory. */
36 | declare function isfolder(path: string): boolean;
37 |
38 | /** Load the given file and return its contents. */
39 | declare function readfile(file: string): string;
40 |
41 | /** Change the given file's contents. Creates a new file if it doesn't exist. */
42 | declare function writefile(file: string, content: string): void;
43 |
44 | /** Returns a list of file paths in the given directory. */
45 | declare function listfiles(directory: string): Array;
46 |
47 | /** Create a new directory at the given path. */
48 | declare function makefolder(directory: string): void;
49 |
50 | /** Removes the directory at the given path. */
51 | declare function delfolder(directory: string): void;
52 |
53 | /** Generates a rbxasset:// [`content`](https://developer.roblox.com/en-us/articles/Content) URL for the given asset from Krnl's `workspace` directory. */
54 | declare function getcustomasset(file: string): string;
55 |
56 | /** Sends an HTTP request using a dictionary to specify the request data, such as the target URL, method, headers and request body data. It returns a dictionary that describes the response data received. */
57 | declare function request(requestOptions: RequestAsyncRequest): RequestAsyncResponse;
58 |
59 | /** Encodes the text to Base64. */
60 | declare function base64_encode(text: string): string;
61 |
62 | /** Encodes the text from Base64. */
63 | declare function base64_decode(base64: string): string;
64 |
65 | /** Hashes the text using the [SHA384](https://en.wikipedia.org/wiki/SHA-2) cipher. */
66 | declare function sha384_hash(text: string): string;
67 |
68 | interface DataModel {
69 | /** Sends an HTTP GET request. */
70 | HttpGetAsync(this: DataModel, url: string): string;
71 |
72 | /** Sends an HTTP POST request. */
73 | HttpPostAsync(this: DataModel, url: string): string; // TODO: Check what it actually returns
74 |
75 | /** Returns an array of Instances associated with the given [`content`](https://developer.roblox.com/en-us/articles/Content) URL. */
76 | GetObjects(this: DataModel, url: string): Instance[];
77 | }
78 |
79 | // Synapse
80 |
81 | declare const syn: {
82 | request: typeof request;
83 | };
84 |
85 | declare const getsynasset: typeof getcustomasset;
86 |
87 | // Scriptware
88 |
89 | declare const http: {
90 | request: typeof request;
91 | };
92 |
93 | // Roblox
94 |
95 | /**
96 | * Sets the environment to be used by the given function. `f` can be a Lua function or a number that specifies the function at that stack level:
97 | * Level 1 is the function calling `setfenv`. `setfenv` returns the given function.
98 | *
99 | * As a special case, when f is 0 setfenv changes the environment of the running thread. In this case, setfenv returns no values.
100 | *
101 | * @param f A Lua function or the stack level.
102 | * @param env The new environment.
103 | */
104 | declare function setfenv(f: T, env: object): T extends 0 ? undefined : T;
105 |
--------------------------------------------------------------------------------
/src/utils/fetch-github-release/downloadRelease.ts:
--------------------------------------------------------------------------------
1 | import { JsonStore } from "utils/JsonStore";
2 | import { downloadAsset } from "./downloadAsset";
3 | import { bootstrap, getRostructPath } from "bootstrap";
4 | import { identify } from "./identify";
5 | import { getLatestRelease, getRelease } from "./getReleases";
6 | import type { FetchInfo } from "./types";
7 |
8 | /** Object used to modify the JSON file with decoded JSON data. */
9 | const savedTags = new JsonStore(getRostructPath("RELEASE_TAGS"));
10 |
11 | /**
12 | * Downloads a release from the given repository. If `asset` is undefined, it downloads
13 | * the source zip files and extracts them. Automatically extracts .zip files.
14 | * This function does not download prereleases or drafts.
15 | * @param owner The owner of the repository.
16 | * @param repo The name of the repository.
17 | * @param tag The release tag to download.
18 | * @param asset Optional asset to download. Defaults to the source files.
19 | * @returns A download result interface.
20 | */
21 | export async function downloadRelease(owner: string, repo: string, tag: string, asset?: string): Promise {
22 | // Type assertions:
23 | assert(type(owner) === "string", "Argument 'owner' must be a string");
24 | assert(type(repo) === "string", "Argument 'repo' must be a string");
25 | assert(type(tag) === "string", "Argument 'tag' must be a string");
26 | assert(asset === undefined || type(asset) === "string", "Argument 'asset' must be a string or nil");
27 |
28 | const id = identify(owner, repo, tag, asset);
29 | const path = getRostructPath("RELEASE_CACHE") + id + "/";
30 |
31 | // If the path is taken, don't download it again
32 | if (isfolder(path))
33 | return {
34 | location: path,
35 | owner: owner,
36 | repo: repo,
37 | tag: tag,
38 | asset: asset ?? "Source code",
39 | updated: false,
40 | };
41 |
42 | const release = await getRelease(owner, repo, tag);
43 | await downloadAsset(release, path, asset);
44 |
45 | return {
46 | location: path,
47 | owner: owner,
48 | repo: repo,
49 | tag: tag,
50 | asset: asset ?? "Source code",
51 | updated: true,
52 | };
53 | }
54 |
55 | /**
56 | * Downloads the latest release from the given repository. If `asset` is undefined,
57 | * it downloads the source zip files and extracts them. Automatically extracts .zip files.
58 | * This function does not download prereleases or drafts.
59 | * @param owner The owner of the repository.
60 | * @param repo The name of the repository.
61 | * @param asset Optional asset to download. Defaults to the source files.
62 | * @returns A download result interface.
63 | */
64 | export async function downloadLatestRelease(owner: string, repo: string, asset?: string): Promise {
65 | // Type assertions:
66 | assert(type(owner) === "string", "Argument 'owner' must be a string");
67 | assert(type(repo) === "string", "Argument 'repo' must be a string");
68 | assert(asset === undefined || type(asset) === "string", "Argument 'asset' must be a string or nil");
69 |
70 | const id = identify(owner, repo, undefined, asset);
71 | const path = getRostructPath("RELEASE_CACHE") + id + "/";
72 |
73 | const release = await getLatestRelease(owner, repo);
74 |
75 | savedTags.open();
76 |
77 | // Check if the cache is up-to-date
78 | if (savedTags.get(id) === release.tag_name && isfolder(path)) {
79 | savedTags.close();
80 | return {
81 | location: path,
82 | owner: owner,
83 | repo: repo,
84 | tag: release.tag_name,
85 | asset: asset ?? "Source code",
86 | updated: false,
87 | };
88 | }
89 |
90 | // Update the cache with the new tag
91 | savedTags.set(id, release.tag_name);
92 | savedTags.close();
93 |
94 | // Make sure nothing is at the path before downloading!
95 | if (isfolder(path)) delfolder(path);
96 |
97 | // Download the asset to the cache
98 | await downloadAsset(release, path, asset);
99 |
100 | return {
101 | location: path,
102 | owner: owner,
103 | repo: repo,
104 | tag: release.tag_name,
105 | asset: asset ?? "Source code",
106 | updated: true,
107 | };
108 | }
109 |
110 | /** Clears the release cache. */
111 | export function clearReleaseCache() {
112 | delfolder(getRostructPath("RELEASE_CACHE"));
113 | bootstrap();
114 | }
115 |
--------------------------------------------------------------------------------
/docs/assets/images/vs-code-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/docs/getting-started/overview.md:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | With **Rostruct**, script executors can run your Lua projects as Roblox Instances. Integrate powerful tools like [Rojo](https://rojo.space/docs/) and [Selene for VS Code](https://marketplace.visualstudio.com/items?itemName=Kampfkarren.selene-vscode) into your workflow, ensuring a hassle-free development experience.
4 |
5 | This documentation is a work in progress!
6 |
7 | ## What you need to know
8 |
9 | This guide assumes:
10 |
11 | - [x] You're familiar with development in an external code editor.
12 | * If you're unsure of how to manage Roblox projects externally, or what we're trying to accomplish, check out the [Rojo docs](https://rojo.space/) for a detailed explanation.
13 |
14 | - [x] You understand how to use Promises.
15 | * Though the Promise-based API is optional, it's very useful. If you'd like to learn more, check out evaera's [Promise library](https://eryn.io/roblox-lua-promise/).
16 |
17 | ## Why Rostruct?
18 |
19 | When it comes to exploiting, projects are often developed and maintained within a single file. However, scripts that get too large become detrimental to your workflow. Over time, your project becomes more difficult to debug, maintain, and share.
20 |
21 | In contrast, with Rojo, your codebase gets turned directly into Roblox Instances. Taking this modular approach to exploiting can significantly improve the development experience.
22 |
23 | Rostruct's design complements a Rojo-based workflow, introducing script developers to a professional way to manage projects.
24 |
25 | ---
26 |
27 | { align=right width=200 draggable=false }
28 |
29 | ### Built for ambitious projects
30 |
31 | Rostruct executes multiple files at once, so you can focus on making your code readable, without worrying about the implementation.
32 |
33 | Create projects from UI libraries to explorers - with little to no limitations.
34 |
35 | ---
36 |
37 | ### Asset management
38 |
39 | Store all of your UI, modules, and assets locally, and they'll be loaded as Roblox objects before runtime.
40 |
41 | Write your code without waiting for assets.
42 |
43 | { align=right width=200 draggable=false }
44 |
45 | ```lua
46 | local midiPlayer = script:FindFirstAncestor("MidiPlayer")
47 |
48 | local Signal = require(midiPlayer.Util.Signal)
49 | local Date = require(midiPlayer.Util.Date)
50 | local Thread = require(midiPlayer.Util.Thread)
51 |
52 | local gui = midiPlayer.Assets.ScreenGui
53 |
54 | gui.Parent = gethui()
55 | ```
56 |
57 | ---
58 |
59 | ### Use projects anywhere
60 |
61 | Want to use a resource? Load Rostruct projects in your script with an intelligent Promise-based module system.
62 |
63 | Seamlessly integrate large projects with an easy-to-use API.
64 |
65 | { align=right width=200 draggable=false }
66 |
67 | ```lua
68 | local Roact = Rostruct.fetchLatest("Roblox", "roact")
69 | :andThen(function(package)
70 | package:build("src/", { Name = "Roact" })
71 | return package:require(package.tree.Roact)
72 | end)
73 | :expect()
74 | ```
75 |
76 | ---
77 |
78 | ### Take advantage of model files
79 |
80 | If you're experienced with GitHub, you can set up a workflow to distribute your project as a `*.rbxm` model file.
81 |
82 | Decrease loading times with model files.
83 |
84 | { align=right width=200 draggable=false }
85 |
86 | ```lua
87 | local Roact = Rostruct.fetchLatest("Roblox", "roact", "Roact.rbxm")
88 | :andThen(function(package)
89 | return package:require(
90 | package:build("Roact.rbxm")
91 | )
92 | end)
93 | :expect()
94 | ```
95 |
96 | ---
97 |
98 | { align=right width=180 draggable=false }
99 |
100 | ### Test at any time
101 |
102 | Design your project with Rojo, a popular tool used to sync an external code editor with Roblox Studio.
103 |
104 | Write code, even during exploit downtime.
105 |
106 | ---
107 |
108 | ### Recommended tools
109 |
110 | Rostruct can (and should!) be paired with helpful tools like:
111 |
112 | * [Rojo](https://rojo.space/docs/) - a project management tool
113 | * [Roblox LSP](https://devforum.roblox.com/t/roblox-lsp-full-intellisense-for-roblox-and-luau/717745) - full intellisense for Roblox and Luau in VS Code
114 | * [Selene for VS Code](https://marketplace.visualstudio.com/items?itemName=Kampfkarren.selene-vscode) - a static analysis tool to help you write better Lua
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Rostruct
4 |
5 |
6 |
7 |
8 |
9 | A modern exploiting solution, built for Roblox and Rojo
10 |
11 |
12 | ---
13 |
14 | **Rostruct** is a project execution framework that runs your code as Roblox instances. This framework substitutes methods that use `HttpGetAsync` and `GetObjects` to load and run code. You can use it with [Rojo](https://rojo.space/) to take advantage of game development tools in your exploits.
15 |
16 | Whether you're familiar with Rojo, want easy asset management, or need some dependencies, you might enjoy using this library.
17 |
18 | See the original concept [here](https://v3rmillion.net/showthread.php?tid=1081675).
19 |
20 | See [rbxm-suite](https://github.com/richie0866/rbxm-suite) for a more efficient solution!
21 |
22 | ## Why Rostruct?
23 |
24 | When it comes to exploiting, projects are often developed and maintained within a single file. However, scripts that get too large become detrimental to your workflow. Over time, your project becomes more difficult to debug, maintain, and share.
25 |
26 | In contrast, with Rojo, your codebase gets turned directly into Roblox Instances. Taking this modular approach to exploiting can significantly improve the development experience.
27 |
28 | Rostruct's design complements a Rojo-based workflow, introducing script developers to a professional way to manage projects.
29 |
30 | ## [Usage](https://richie0866.github.io/Rostruct)
31 |
32 | Documentation is available at the [GitHub Pages site](https://richie0866.github.io/Rostruct).
33 |
34 | ## How it works
35 |
36 |
38 |
39 | Rostruct builds instances following a [file conversion model](https://richie0866.github.io/Rostruct/api-reference/file-conversion/). Files compatible with Rostruct (`lua`, `json`, `rbxm`, etc.) are turned into Roblox instances.
40 |
41 | Scripts have preset `script` and `require` globals to mirror LocalScript and ModuleScript objects. This way, runtime is similar between Roblox Studio and script executors.
42 |
43 | ## Features
44 |
45 | * Promotes modular programming
46 | * Keep your codebase readable and maintainable.
47 |
48 | * Instance-based execution
49 | * Write your code like it's Roblox Studio.
50 |
51 | * GitHub support
52 | * Build packages from GitHub releases, allowing users to execute your code without manually downloading it.
53 |
54 | * Builds `rbxm` models
55 | * Go `GetObjects`-free by including assets in your project files.
56 |
57 | * Designed for [Rojo](https://github.com/rojo-rbx/rojo#readme)
58 | * Test your code in Roblox Studio without an exploit.
59 |
60 | ## Contributing
61 |
62 | If there are any features you think are missing, or the code can be improved, feel free to [open an issue](https://github.com/richie0866/Rostruct/issues)!
63 |
64 | If you'd like to contribute, [fork Rostruct](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and submit a [pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) when you're ready.
65 |
66 | ### Setup
67 |
68 | 1. Run `npm install` to install all dependencies used by this project.
69 | 2. Then, run `rbxtsc -w` to start building the TypeScript source to Lua files as you make changes.
70 |
71 | ### Testing
72 |
73 | 3. Run `npm run build:prod` in a Git Bash terminal to generate an output file. Make sure the `out/` directory is up-to-date with `rbxtsc`!
74 | 4. The command should create a `Rostruct.lua` file in the root directory. At the end of the script, you'll find this code:
75 |
76 | ```lua
77 | return Rostruct
78 | ```
79 | You can replace that line with code that uses Rostruct.
80 |
81 | ## License
82 |
83 | Rostruct is available under the MIT license. See [LICENSE](https://github.com/richie0866/Rostruct/blob/main/LICENSE) for more details.
84 |
--------------------------------------------------------------------------------
/docs/getting-started/publishing-your-project.md:
--------------------------------------------------------------------------------
1 | # Publishing your project
2 |
3 | Once you're ready to distribute your project, it's important to note that it should run automatically. Ideally, the end-user shouldn't do more than save a script.
4 |
5 | Fortunately, Rostruct provides functionality to deploy your codebase through GitHub:
6 |
7 | ## Deploying from GitHub
8 |
9 | The best way to publish your Rostruct project is by creating [GitHub Releases](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository) in a repository. Rostruct will save and version-check the releases for you.
10 |
11 | You should write a **retriever** script that lets users run your project without any extra setup.
12 | The **retriever** handles the execution of your repo's latest GitHub Release. However, you'll need to load Rostruct to do that.
13 |
14 | ## Loading Rostruct
15 |
16 | In the retriever, you can load Rostruct in two ways: with an HTTP request, or from Rostruct's source code. Each option has its pros and cons, so choose whichever one best fits your project.
17 |
18 | ### with HTTP GET recommended { data-toc-label="with HTTP GET" }
19 |
20 | If you prefer a quick and concise way to load Rostruct, you can load it with an HTTP request.
21 |
22 | To do this, you should pick a release from the [GitHub Releases page](https://github.com/richie0866/Rostruct/releases/latest), and copy the **tag version** to `TAG_VERSION_HERE` in this code:
23 |
24 | ```lua hl_lines="3"
25 | local Rostruct = loadstring(game:HttpGetAsync(
26 | "https://github.com/richie0866/Rostruct/releases/download/"
27 | .. "TAG_VERSION_HERE"
28 | .. "/Rostruct.lua"
29 | ))()
30 | ```
31 |
32 | This loads the Rostruct library by getting the source and executing it. You can now [deploy your project](#deployment).
33 |
34 | ### with source code
35 |
36 | If you don't want to make an HTTP request, you can load Rostruct instantly by using the source code in your script. In other words, you'll be using Rostruct as an **internal module**.
37 |
38 | To add Rostruct's source to your retriever, download the `Rostruct.lua` asset from your preferred release of Rostruct from the [GitHub Releases page](https://github.com/richie0866/Rostruct/releases/latest), and paste it into your retriever.
39 |
40 | This file should end with `#!lua return Rostruct`. Since you're going to use Rostruct, all you have to do is remove that line!
41 |
42 | Although this bloats up your file, unlike the first method, you can use Rostruct immediately.
43 |
44 | ## Running your project
45 |
46 | After you've loaded Rostruct, use [`Rostruct.fetch`](../api-reference/rostruct/fetch.md) or [`Rostruct.fetchLatest`](../api-reference/rostruct/fetchlatest.md) to download and package the release files in your retriever.
47 |
48 | ### Deployment
49 |
50 | You can deploy your project using Rostruct's `fetch` functions, which return a Promise that resolves with a new [`Package`](../api-reference/package/properties.md) object. It functions almost identically to using `Rostruct.open`, just with a Promise:
51 |
52 | === "Start"
53 |
54 | ```lua
55 | -- Download the latest release to local files
56 | return Rostruct.fetchLatest("richie0866", "MidiPlayer")
57 | -- Then, build and start all scripts
58 | :andThen(function(package)
59 | package:build("src/")
60 | package:start()
61 | return package
62 | end)
63 | -- Finally, wait until the Promise is done
64 | :expect()
65 | ```
66 |
67 | === "Require"
68 |
69 | ```lua
70 | -- Download the latest release of Roact to local files
71 | return Rostruct.fetchLatest("Roblox", "roact")
72 | -- Then, build and require Roact
73 | :andThen(function(package)
74 | return package:require(
75 | package:build("src/", { Name = "Roact" })
76 | )
77 | end)
78 | -- Finally, wait until the Promise is done, and
79 | -- return the result of package:require
80 | :expect()
81 | ```
82 |
83 | Now, anyone with this script can deploy your project in a Roblox script executor. Remember to test your code!
84 |
85 | You can simplify the script for end-user by saving the retriever in your repo and loading its source with `HttpGetAsync`:
86 |
87 | ### Distribution
88 |
89 | Some users may prefer a short and concise way to use your project. To account for this, you should provide additional code that uses the `loadstring-HttpGet` pattern to run your project's retriever.
90 |
91 | Loading your module through an HTTP request may seem counterproductive, but some developers may prefer it in a single-file project. So, your code should look something like this:
92 |
93 | ```lua
94 | local Foo = loadstring(game:HttpGetAsync("LINK_TO_RAW_RETRIEVER"))()
95 | ```
96 |
97 | `RAW_RETRIEVER_URL` should be replaced with a link to your retriever's raw contents. It may be in your best interest to [load Rostruct internally](#with-source-code) to avoid the extra HTTP request.
98 |
--------------------------------------------------------------------------------
/src/modules/make/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | declare type WritablePropertyNames = {
4 | readonly [K in keyof T]-?: T[K] extends Callback
5 | ? never
6 | : (() => F extends {
7 | [Q in K]: T[K];
8 | }
9 | ? 1
10 | : 2) extends () => F extends {
11 | -readonly [Q in K]: T[K];
12 | }
13 | ? 1
14 | : 2
15 | ? K
16 | : never;
17 | }[keyof T];
18 | declare type GetBindableToRBXScriptSignal = {
19 | [key in {
20 | [K in keyof T]-?: T[K] extends RBXScriptSignal ? K : never;
21 | }[keyof T]]: T[key] extends RBXScriptSignal ? R : never;
22 | };
23 | /**
24 | * Returns a table wherein an object's writable properties can be specified,
25 | * while also allowing functions to be passed in which can be bound to a RBXScriptSignal.
26 | */
27 | declare type GetPartialObjectWithBindableConnectSlots = Partial<
28 | Pick> & GetBindableToRBXScriptSignal
29 | >;
30 | /**
31 | * Instantiates a new Instance of `className` with given `settings`,
32 | * where `settings` is an object of the form { [K: propertyName]: value }.
33 | *
34 | * `settings.Children` is an array of child objects to be parented to the generated Instance.
35 | *
36 | * Events can be set to a callback function, which will be connected.
37 | *
38 | * `settings.Parent` is always set last.
39 | */
40 | declare function Make<
41 | T extends keyof CreatableInstances,
42 | Q extends GetPartialObjectWithBindableConnectSlots & {
43 | /** The Children to place inside of this Instance. */
44 | Children?: ReadonlyArray;
45 | Parent?: Instance | undefined;
46 | },
47 | >(
48 | className: T,
49 | settings: Q,
50 | ): CreatableInstances[T] &
51 | {
52 | [K_1 in keyof ({ [O in Extract<"Name", keyof Q>]: Q[O] } & {
53 | ClassName: T;
54 | } & (Q["Children"] extends never
55 | ? never
56 | : {
57 | [K in Exclude<
58 | keyof Q["Children"],
59 | | number
60 | | "find"
61 | | "size"
62 | | "isEmpty"
63 | | "join"
64 | | "move"
65 | | "includes"
66 | | "indexOf"
67 | | "every"
68 | | "some"
69 | | "forEach"
70 | | "map"
71 | | "mapFiltered"
72 | | "filterUndefined"
73 | | "filter"
74 | | "reduce"
75 | | "findIndex"
76 | | "_nominal_Array"
77 | | "length"
78 | >]: Q["Children"][K] extends infer A
79 | ? A extends {
80 | Name: string;
81 | }
82 | ? string extends A["Name"]
83 | ? never
84 | : (k: { [P in A["Name"]]: A }) => void
85 | : never
86 | : never;
87 | }[Exclude<
88 | keyof Q["Children"],
89 | | number
90 | | "find"
91 | | "size"
92 | | "isEmpty"
93 | | "join"
94 | | "move"
95 | | "includes"
96 | | "indexOf"
97 | | "every"
98 | | "some"
99 | | "forEach"
100 | | "map"
101 | | "mapFiltered"
102 | | "filterUndefined"
103 | | "filter"
104 | | "reduce"
105 | | "findIndex"
106 | | "_nominal_Array"
107 | | "length"
108 | >] extends (k: infer U) => void
109 | ? U
110 | : never))]: ({ [O in Extract<"Name", keyof Q>]: Q[O] } & {
111 | ClassName: T;
112 | } & (Q["Children"] extends never
113 | ? never
114 | : {
115 | [K in Exclude<
116 | keyof Q["Children"],
117 | | number
118 | | "find"
119 | | "size"
120 | | "isEmpty"
121 | | "join"
122 | | "move"
123 | | "includes"
124 | | "indexOf"
125 | | "every"
126 | | "some"
127 | | "forEach"
128 | | "map"
129 | | "mapFiltered"
130 | | "filterUndefined"
131 | | "filter"
132 | | "reduce"
133 | | "findIndex"
134 | | "_nominal_Array"
135 | | "length"
136 | >]: Q["Children"][K] extends infer A
137 | ? A extends {
138 | Name: string;
139 | }
140 | ? string extends A["Name"]
141 | ? never
142 | : (k: { [P in A["Name"]]: A }) => void
143 | : never
144 | : never;
145 | }[Exclude<
146 | keyof Q["Children"],
147 | | number
148 | | "find"
149 | | "size"
150 | | "isEmpty"
151 | | "join"
152 | | "move"
153 | | "includes"
154 | | "indexOf"
155 | | "every"
156 | | "some"
157 | | "forEach"
158 | | "map"
159 | | "mapFiltered"
160 | | "filterUndefined"
161 | | "filter"
162 | | "reduce"
163 | | "findIndex"
164 | | "_nominal_Array"
165 | | "length"
166 | >] extends (k: infer U) => void
167 | ? U
168 | : never))[K_1];
169 | };
170 | export = Make;
171 |
--------------------------------------------------------------------------------
/docs/assets/overrides/home.html:
--------------------------------------------------------------------------------
1 | {% extends "main.html" %}
2 |
3 |
4 | {% block tabs %}
5 | {{ super() }}
6 |
7 |
8 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
Take exploiting to the next level
246 |
Manage your Lua projects using professional-grade tools to run and publish your code. Deploy your project files to a Roblox script executor with a simple and flexible execution library.
247 |
248 | Get started
249 |
250 |
251 | Go to GitHub
252 |
253 |
254 |
255 |
256 |
257 |
258 | {% endblock %}
259 |
260 |
261 | {% block content %}{% endblock %}
262 |
263 |
264 | {% block footer %}{% endblock %}
265 |
--------------------------------------------------------------------------------
/src/core/VirtualScript.ts:
--------------------------------------------------------------------------------
1 | import { Store } from "./Store";
2 | import { HttpService } from "modules/services";
3 | import type { Executor, VirtualEnvironment } from "./types";
4 |
5 | /** Maps scripts to the module they're loading, like a history of `[script who loaded]: module` */
6 | const currentlyLoading = new Map();
7 |
8 | /**
9 | * Check if a module contains a cyclic dependency chain.
10 | * @param module The starting VirtualScript.
11 | */
12 | function checkTraceback(module: VirtualScript) {
13 | let currentModule: VirtualScript | undefined = module;
14 | let depth = 0;
15 | while (currentModule) {
16 | depth += 1;
17 | currentModule = currentlyLoading.get(currentModule);
18 |
19 | // If the loop reaches 'module' again, this is a cyclic reference.
20 | if (module === currentModule) {
21 | let traceback = module.getChunkName();
22 |
23 | // Create a string to represent the dependency chain.
24 | for (let i = 0; i < depth; i++) {
25 | currentModule = currentlyLoading.get(currentModule)!;
26 | traceback += `\n\t\t⇒ ${currentModule.getChunkName()}`;
27 | }
28 |
29 | throw (
30 | `Requested module '${module.getChunkName()}' contains a cyclic reference` +
31 | `\n\tTraceback: ${traceback}`
32 | );
33 | }
34 | }
35 | }
36 |
37 | /** Manages file execution. */
38 | export class VirtualScript {
39 | /** Maps VirtualScripts to their instances. */
40 | private static readonly fromInstance = Store.getStore("VirtualScriptStore");
41 |
42 | /** An identifier for this VirtualScript. */
43 | public readonly id = "VirtualScript-" + HttpService.GenerateGUID(false);
44 |
45 | /** The function to be called at runtime when the script runs or gets required. */
46 | private executor?: Executor;
47 |
48 | /** The executor's return value after being called the first time. ModuleScripts must have a non-nil result. */
49 | private result?: unknown;
50 |
51 | /** Whether the executor has already been called. */
52 | private jobComplete = false;
53 |
54 | /** A custom environment used during runtime. */
55 | private readonly scriptEnvironment: VirtualEnvironment;
56 |
57 | constructor(
58 | /** The Instance that represents this object used for globals. */
59 | public readonly instance: LuaSourceContainer,
60 |
61 | /** The file this object extends. */
62 | public readonly path: string,
63 |
64 | /** The root directory. */
65 | public readonly root: string,
66 |
67 | /** The contents of the file. */
68 | public readonly source = readfile(path),
69 | ) {
70 | this.scriptEnvironment = setmetatable(
71 | {
72 | script: instance,
73 | require: (obj: ModuleScript) => VirtualScript.loadModule(obj, this),
74 | _PATH: path,
75 | _ROOT: root,
76 | },
77 | {
78 | __index: getfenv(0) as never,
79 | __metatable: "This metatable is locked",
80 | },
81 | );
82 | VirtualScript.fromInstance.set(instance, this);
83 | }
84 |
85 | /**
86 | * Gets an existing VirtualScript for a specific instance.
87 | * @param object
88 | */
89 | public static getFromInstance(object: LuaSourceContainer): VirtualScript | undefined {
90 | return this.fromInstance.get(object);
91 | }
92 |
93 | /**
94 | * Executes a `VirtualScript` from the given module and returns the result.
95 | * @param object The ModuleScript to require.
96 | * @returns What the module returned.
97 | */
98 | public static requireFromInstance(object: ModuleScript): unknown {
99 | const module = this.getFromInstance(object);
100 | assert(module, `Failed to get VirtualScript for Instance '${object.GetFullName()}'`);
101 | return module.runExecutor();
102 | }
103 |
104 | /**
105 | * Requires a `ModuleScript`. If the module has a `VirtualScript` counterpart,
106 | * it calls `VirtualScript.execute` and returns the result.
107 | *
108 | * Detects recursive references using roblox-ts's RuntimeLib solution.
109 | * The original source of this module can be found in the link below, as well as the license:
110 | * - Source: https://github.com/roblox-ts/roblox-ts/blob/master/lib/RuntimeLib.lua
111 | * - License: https://github.com/roblox-ts/roblox-ts/blob/master/LICENSE
112 | *
113 | * @param object The ModuleScript to require.
114 | * @param caller The calling VirtualScript.
115 | * @returns What the module returned.
116 | */
117 | private static loadModule(object: ModuleScript, caller: VirtualScript): unknown {
118 | const module = this.fromInstance.get(object);
119 | if (!module) return require(object);
120 |
121 | currentlyLoading.set(caller, module);
122 |
123 | // Check for a cyclic dependency
124 | checkTraceback(module);
125 |
126 | const result = module.runExecutor();
127 |
128 | // Thread-safe cleanup avoids overwriting other loading modules
129 | if (currentlyLoading.get(caller) === module) currentlyLoading.delete(caller);
130 |
131 | return result;
132 | }
133 |
134 | /**
135 | * Returns the chunk name for the module for traceback.
136 | */
137 | public getChunkName() {
138 | const file = this.path.sub(this.root.size() + 1);
139 | return `@${file} (${this.instance.GetFullName()})`;
140 | }
141 |
142 | /**
143 | * Sets the executor function.
144 | * @param exec The function to call on execution.
145 | */
146 | public setExecutor(exec: Executor) {
147 | assert(this.jobComplete === false, "Cannot set executor after script was executed");
148 | this.executor = exec;
149 | }
150 |
151 | /**
152 | * Gets or creates a new executor function, and returns the executor function.
153 | * The executor is automatically given a special global environment.
154 | * @returns The executor function.
155 | */
156 | public createExecutor(): Executor {
157 | if (this.executor) return this.executor;
158 | const [f, err] = loadstring(this.source, `=${this.getChunkName()}`);
159 | assert(f, err);
160 | return (this.executor = setfenv(f, this.scriptEnvironment));
161 | }
162 |
163 | /**
164 | * Runs the executor function if not already run and returns results.
165 | * @returns The value returned by the executor.
166 | */
167 | public runExecutor(): T {
168 | if (this.jobComplete) return this.result as never;
169 |
170 | const result = this.createExecutor()(this.scriptEnvironment);
171 |
172 | if (this.instance.IsA("ModuleScript") && result === undefined)
173 | throw `Module '${this.getChunkName()}' did not return any value`;
174 |
175 | this.jobComplete = true;
176 |
177 | return (this.result = result) as never;
178 | }
179 |
180 | /**
181 | * Runs the executor function if not already run and returns results.
182 | * @returns A promise which resolves with the value returned by the executor.
183 | */
184 | public deferExecutor(): Promise {
185 | return Promise.defer((resolve) => resolve(this.runExecutor())).timeout(
186 | 30,
187 | `Script ${this.getChunkName()} reached execution timeout! Try not to yield the main thread in LocalScripts.`,
188 | );
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/bin/runtime.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | Originally RuntimeLib.lua supplied by roblox-ts, modified for use when bundled.
3 | The original source of this module can be found in the link below, as well as the license:
4 |
5 | https://github.com/roblox-ts/roblox-ts/blob/master/lib/RuntimeLib.lua
6 | https://github.com/roblox-ts/roblox-ts/blob/master/LICENSE
7 | ]]
8 |
9 | local TS = {
10 | _G = {};
11 | }
12 |
13 | setmetatable(TS, {
14 | __index = function(self, k)
15 | if k == "Promise" then
16 | self.Promise = TS.initialize("modules", "Promise")
17 | return self.Promise
18 | end
19 | end
20 | })
21 |
22 | -- Runtime classes
23 | local FilePtr
24 | do
25 | FilePtr = {}
26 | FilePtr.__index = FilePtr
27 |
28 | function FilePtr.new(path)
29 | local fileName, slash = string.match(path, "([^/]+)(/*)$")
30 | return setmetatable({
31 | name = fileName,
32 | path = string.sub(path, 1, -#fileName - (slash ~= "" and 2 or 1)),
33 | }, FilePtr)
34 | end
35 |
36 | function FilePtr:__index(k)
37 | if k == "Parent" then
38 | return FilePtr.new(self.path)
39 | end
40 | end
41 |
42 | end
43 |
44 | local Module
45 | do
46 | Module = {}
47 | Module.__index = Module
48 |
49 | function Module.new(path, name, func)
50 | return setmetatable({
51 | -- Init files are representations of their parent directory,
52 | -- so if it's an init file, we trim the "/init.lua" off of
53 | -- the file path.
54 | path = name ~= "init"
55 | and path
56 | or FilePtr.new(path).path,
57 | name = name,
58 | func = func,
59 | data = nil;
60 | }, Module)
61 | end
62 |
63 | function Module:__index(k)
64 | if Module[k] then
65 | return Module[k]
66 | elseif k == "Parent" then
67 | return FilePtr.new(self.path)
68 | elseif k == "Name" then
69 | return self.path
70 | end
71 | end
72 |
73 | function Module:require()
74 | if self.func then
75 | self.data = self.func()
76 | self.func = nil
77 | end
78 | return self.data
79 | end
80 |
81 | function Module:GetFullName()
82 | return self.path
83 | end
84 |
85 | end
86 |
87 | local Symbol
88 | do
89 | Symbol = {}
90 | Symbol.__index = Symbol
91 | setmetatable(
92 | Symbol,
93 | {
94 | __call = function(_, description)
95 | local self = setmetatable({}, Symbol)
96 | self.description = "Symbol(" .. (description or "") .. ")"
97 | return self
98 | end,
99 | }
100 | )
101 |
102 | local symbolRegistry = setmetatable(
103 | {},
104 | {
105 | __index = function(self, k)
106 | self[k] = Symbol(k)
107 | return self[k]
108 | end,
109 | }
110 | )
111 |
112 | function Symbol:toString()
113 | return self.description
114 | end
115 |
116 | Symbol.__tostring = Symbol.toString
117 |
118 | -- Symbol.for
119 | function Symbol.getFor(key)
120 | return symbolRegistry[key]
121 | end
122 |
123 | function Symbol.keyFor(goalSymbol)
124 | for key, symbol in pairs(symbolRegistry) do
125 | if symbol == goalSymbol then
126 | return key
127 | end
128 | end
129 | end
130 | end
131 |
132 | TS.Symbol = Symbol
133 | TS.Symbol_iterator = Symbol("Symbol.iterator")
134 |
135 | -- Provides a way to attribute modules to files.
136 | local modulesByPath = {}
137 | local modulesByName = {}
138 |
139 | -- Bundle compatibility
140 | function TS.register(path, name, func)
141 | local module = Module.new(path, name, func)
142 | modulesByPath[path] = module
143 | modulesByName[name] = module
144 | return module
145 | end
146 |
147 | function TS.get(path)
148 | return modulesByPath[path]
149 | end
150 |
151 | function TS.initialize(...)
152 | local symbol = setmetatable({}, {__tostring = function()
153 | return "root"
154 | end})
155 | local caller = TS.register(symbol, symbol)
156 | return TS.import(caller, { path = "out/" }, ...)
157 | end
158 |
159 | -- module resolution
160 | function TS.getModule(_object, _moduleName)
161 | return error("TS.getModule is not supported", 2)
162 | end
163 |
164 | -- This is a hash which TS.import uses as a kind of linked-list-like history of [Script who Loaded] -> Library
165 | local currentlyLoading = {}
166 | local registeredLibraries = {}
167 |
168 | function TS.import(caller, parentPtr, ...)
169 | -- Because 'Module.Parent' returns a FilePtr, the module handles the indexing.
170 | -- Getting 'parentPtr.path' will return the result of FilePtr.Parent.Parent...
171 | local modulePath = parentPtr.path .. table.concat({...}, "/") .. ".lua"
172 | local moduleInit = parentPtr.path .. table.concat({...}, "/") .. "/init.lua"
173 | local module = assert(
174 | modulesByPath[modulePath] or modulesByPath[moduleInit],
175 | "No module exists at path '" .. modulePath .. "'"
176 | )
177 |
178 | currentlyLoading[caller] = module
179 |
180 | -- Check to see if a case like this occurs:
181 | -- module -> Module1 -> Module2 -> module
182 |
183 | -- WHERE currentlyLoading[module] is Module1
184 | -- and currentlyLoading[Module1] is Module2
185 | -- and currentlyLoading[Module2] is module
186 |
187 | local currentModule = module
188 | local depth = 0
189 |
190 | while currentModule do
191 | depth = depth + 1
192 | currentModule = currentlyLoading[currentModule]
193 |
194 | if currentModule == module then
195 | local str = currentModule.name -- Get the string traceback
196 |
197 | for _ = 1, depth do
198 | currentModule = currentlyLoading[currentModule]
199 | str ..= " => " .. currentModule.name
200 | end
201 |
202 | error("Failed to import! Detected a circular dependency chain: " .. str, 2)
203 | end
204 | end
205 |
206 | if not registeredLibraries[module] then
207 | if TS._G[module] then
208 | error(
209 | "Invalid module access! Do you have two TS runtimes trying to import this? " .. module.path,
210 | 2
211 | )
212 | end
213 |
214 | TS._G[module] = TS
215 | registeredLibraries[module] = true -- register as already loaded for subsequent calls
216 | end
217 |
218 | local data = module:require()
219 |
220 | if currentlyLoading[caller] == module then -- Thread-safe cleanup!
221 | currentlyLoading[caller] = nil
222 | end
223 |
224 | return data
225 | end
226 |
227 | -- general utility functions
228 | function TS.async(callback)
229 | local Promise = TS.Promise
230 | return function(...)
231 | local n = select("#", ...)
232 | local args = { ... }
233 | return Promise.new(function(resolve, reject)
234 | coroutine.wrap(function()
235 | local ok, result = pcall(callback, unpack(args, 1, n))
236 | if ok then
237 | resolve(result)
238 | else
239 | reject(result)
240 | end
241 | end)()
242 | end)
243 | end
244 | end
245 |
246 | function TS.await(promise)
247 | local Promise = TS.Promise
248 | if not Promise.is(promise) then
249 | return promise
250 | end
251 |
252 | local status, value = promise:awaitStatus()
253 | if status == Promise.Status.Resolved then
254 | return value
255 | elseif status == Promise.Status.Rejected then
256 | error(value, 2)
257 | else
258 | error("The awaited Promise was cancelled", 2)
259 | end
260 | end
261 |
262 | -- opcall
263 |
264 | function TS.opcall(func, ...)
265 | local success, valueOrErr = pcall(func, ...)
266 | if success then
267 | return {
268 | success = true,
269 | value = valueOrErr,
270 | }
271 | else
272 | return {
273 | success = false,
274 | error = valueOrErr,
275 | }
276 | end
277 | end
278 |
--------------------------------------------------------------------------------
/src/modules/zzlib/init.lua:
--------------------------------------------------------------------------------
1 | -- zzlib - zlib decompression in Lua - Implementation-independent code
2 |
3 | -- Copyright (c) 2016-2020 Francois Galea
4 | -- This program is free software. It comes without any warranty, to
5 | -- the extent permitted by applicable law. You can redistribute it
6 | -- and/or modify it under the terms of the Do What The Fuck You Want
7 | -- To Public License, Version 2, as published by Sam Hocevar. See
8 | -- the COPYING file or http://www.wtfpl.net/ for more details.
9 |
10 |
11 | local unpack = unpack
12 | local result
13 |
14 | local infl do
15 | local inflate = {}
16 |
17 | local bit = bit32
18 |
19 | inflate.band = bit.band
20 | inflate.rshift = bit.rshift
21 |
22 | function inflate.bitstream_init(file)
23 | local bs = {
24 | file = file, -- the open file handle
25 | buf = nil, -- character buffer
26 | len = nil, -- length of character buffer
27 | pos = 1, -- position in char buffer
28 | b = 0, -- bit buffer
29 | n = 0, -- number of bits in buffer
30 | }
31 | -- get rid of n first bits
32 | function bs:flushb(n)
33 | self.n = self.n - n
34 | self.b = bit.rshift(self.b,n)
35 | end
36 | -- peek a number of n bits from stream
37 | function bs:peekb(n)
38 | while self.n < n do
39 | if self.pos > self.len then
40 | self.buf = self.file:read(4096)
41 | self.len = self.buf:len()
42 | self.pos = 1
43 | end
44 | self.b = self.b + bit.lshift(self.buf:byte(self.pos),self.n)
45 | self.pos = self.pos + 1
46 | self.n = self.n + 8
47 | end
48 | return bit.band(self.b,bit.lshift(1,n)-1)
49 | end
50 | -- get a number of n bits from stream
51 | function bs:getb(n)
52 | local ret = bs:peekb(n)
53 | self.n = self.n - n
54 | self.b = bit.rshift(self.b,n)
55 | return ret
56 | end
57 | -- get next variable-size of maximum size=n element from stream, according to Huffman table
58 | function bs:getv(hufftable,n)
59 | local e = hufftable[bs:peekb(n)]
60 | local len = bit.band(e,15)
61 | local ret = bit.rshift(e,4)
62 | self.n = self.n - len
63 | self.b = bit.rshift(self.b,len)
64 | return ret
65 | end
66 | function bs:close()
67 | if self.file then
68 | self.file:close()
69 | end
70 | end
71 | if type(file) == "string" then
72 | bs.file = nil
73 | bs.buf = file
74 | else
75 | bs.buf = file:read(4096)
76 | end
77 | bs.len = bs.buf:len()
78 | return bs
79 | end
80 |
81 | local function hufftable_create(depths)
82 | local nvalues = #depths
83 | local nbits = 1
84 | local bl_count = {}
85 | local next_code = {}
86 | for i=1,nvalues do
87 | local d = depths[i]
88 | if d > nbits then
89 | nbits = d
90 | end
91 | bl_count[d] = (bl_count[d] or 0) + 1
92 | end
93 | local table = {}
94 | local code = 0
95 | bl_count[0] = 0
96 | for i=1,nbits do
97 | code = (code + (bl_count[i-1] or 0)) * 2
98 | next_code[i] = code
99 | end
100 | for i=1,nvalues do
101 | local len = depths[i] or 0
102 | if len > 0 then
103 | local e = (i-1)*16 + len
104 | local code = next_code[len]
105 | local rcode = 0
106 | for j=1,len do
107 | rcode = rcode + bit.lshift(bit.band(1,bit.rshift(code,j-1)),len-j)
108 | end
109 | for j=0,2^nbits-1,2^len do
110 | table[j+rcode] = e
111 | end
112 | next_code[len] = next_code[len] + 1
113 | end
114 | end
115 | return table,nbits
116 | end
117 |
118 | local function block_loop(out,bs,nlit,ndist,littable,disttable)
119 | local lit
120 | repeat
121 | lit = bs:getv(littable,nlit)
122 | if lit < 256 then
123 | table.insert(out,lit)
124 | elseif lit > 256 then
125 | local nbits = 0
126 | local size = 3
127 | local dist = 1
128 | if lit < 265 then
129 | size = size + lit - 257
130 | elseif lit < 285 then
131 | nbits = bit.rshift(lit-261,2)
132 | size = size + bit.lshift(bit.band(lit-261,3)+4,nbits)
133 | else
134 | size = 258
135 | end
136 | if nbits > 0 then
137 | size = size + bs:getb(nbits)
138 | end
139 | local v = bs:getv(disttable,ndist)
140 | if v < 4 then
141 | dist = dist + v
142 | else
143 | nbits = bit.rshift(v-2,1)
144 | dist = dist + bit.lshift(bit.band(v,1)+2,nbits)
145 | dist = dist + bs:getb(nbits)
146 | end
147 | local p = #out-dist+1
148 | while size > 0 do
149 | table.insert(out,out[p])
150 | p = p + 1
151 | size = size - 1
152 | end
153 | end
154 | until lit == 256
155 | end
156 |
157 | local function block_dynamic(out,bs)
158 | local order = { 17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16 }
159 | local hlit = 257 + bs:getb(5)
160 | local hdist = 1 + bs:getb(5)
161 | local hclen = 4 + bs:getb(4)
162 | local depths = {}
163 | for i=1,hclen do
164 | local v = bs:getb(3)
165 | depths[order[i]] = v
166 | end
167 | for i=hclen+1,19 do
168 | depths[order[i]] = 0
169 | end
170 | local lengthtable,nlen = hufftable_create(depths)
171 | local i=1
172 | while i<=hlit+hdist do
173 | local v = bs:getv(lengthtable,nlen)
174 | if v < 16 then
175 | depths[i] = v
176 | i = i + 1
177 | elseif v < 19 then
178 | local nbt = {2,3,7}
179 | local nb = nbt[v-15]
180 | local c = 0
181 | local n = 3 + bs:getb(nb)
182 | if v == 16 then
183 | c = depths[i-1]
184 | elseif v == 18 then
185 | n = n + 8
186 | end
187 | for j=1,n do
188 | depths[i] = c
189 | i = i + 1
190 | end
191 | else
192 | error("wrong entry in depth table for literal/length alphabet: "..v);
193 | end
194 | end
195 | local litdepths = {} for i=1,hlit do table.insert(litdepths,depths[i]) end
196 | local littable,nlit = hufftable_create(litdepths)
197 | local distdepths = {} for i=hlit+1,#depths do table.insert(distdepths,depths[i]) end
198 | local disttable,ndist = hufftable_create(distdepths)
199 | block_loop(out,bs,nlit,ndist,littable,disttable)
200 | end
201 |
202 | local function block_static(out,bs)
203 | local cnt = { 144, 112, 24, 8 }
204 | local dpt = { 8, 9, 7, 8 }
205 | local depths = {}
206 | for i=1,4 do
207 | local d = dpt[i]
208 | for j=1,cnt[i] do
209 | table.insert(depths,d)
210 | end
211 | end
212 | local littable,nlit = hufftable_create(depths)
213 | depths = {}
214 | for i=1,32 do
215 | depths[i] = 5
216 | end
217 | local disttable,ndist = hufftable_create(depths)
218 | block_loop(out,bs,nlit,ndist,littable,disttable)
219 | end
220 |
221 | local function block_uncompressed(out,bs)
222 | bs:flushb(bit.band(bs.n,7))
223 | local len = bs:getb(16)
224 | if bs.n > 0 then
225 | error("Unexpected.. should be zero remaining bits in buffer.")
226 | end
227 | local nlen = bs:getb(16)
228 | if bit.bxor(len,nlen) ~= 65535 then
229 | error("LEN and NLEN don't match")
230 | end
231 | for i=bs.pos,bs.pos+len-1 do
232 | table.insert(out,bs.buf:byte(i,i))
233 | end
234 | bs.pos = bs.pos + len
235 | end
236 |
237 | function inflate.main(bs)
238 | local last,type
239 | local output = {}
240 | repeat
241 | local block
242 | last = bs:getb(1)
243 | type = bs:getb(2)
244 | if type == 0 then
245 | block_uncompressed(output,bs)
246 | elseif type == 1 then
247 | block_static(output,bs)
248 | elseif type == 2 then
249 | block_dynamic(output,bs)
250 | else
251 | error("unsupported block type")
252 | end
253 | until last == 1
254 | bs:flushb(bit.band(bs.n,7))
255 | return output
256 | end
257 |
258 | local crc32_table
259 | function inflate.crc32(s,crc)
260 | if not crc32_table then
261 | crc32_table = {}
262 | for i=0,255 do
263 | local r=i
264 | for j=1,8 do
265 | r = bit.bxor(bit.rshift(r,1),bit.band(0xedb88320,bit.bnot(bit.band(r,1)-1)))
266 | end
267 | crc32_table[i] = r
268 | end
269 | end
270 | crc = bit.bnot(crc or 0)
271 | for i=1,#s do
272 | local c = s:byte(i)
273 | crc = bit.bxor(crc32_table[bit.bxor(c,bit.band(crc,0xff))],bit.rshift(crc,8))
274 | end
275 | crc = bit.bnot(crc)
276 | if crc<0 then
277 | -- in Lua < 5.2, sign extension was performed
278 | crc = crc + 4294967296
279 | end
280 | return crc
281 | end
282 |
283 | infl = inflate
284 | end
285 |
286 | local zzlib = {}
287 |
288 | local function arraytostr(array)
289 | local tmp = {}
290 | local size = #array
291 | local pos = 1
292 | local imax = 1
293 | while size > 0 do
294 | local bsize = size>=2048 and 2048 or size
295 | local s = string.char(unpack(array,pos,pos+bsize-1))
296 | pos = pos + bsize
297 | size = size - bsize
298 | local i = 1
299 | while tmp[i] do
300 | s = tmp[i]..s
301 | tmp[i] = nil
302 | i = i + 1
303 | end
304 | if i > imax then
305 | imax = i
306 | end
307 | tmp[i] = s
308 | end
309 | local str = ""
310 | for i=1,imax do
311 | if tmp[i] then
312 | str = tmp[i]..str
313 | end
314 | end
315 | return str
316 | end
317 |
318 | local function inflate_gzip(bs)
319 | local id1,id2,cm,flg = bs.buf:byte(1,4)
320 | if id1 ~= 31 or id2 ~= 139 then
321 | error("invalid gzip header")
322 | end
323 | if cm ~= 8 then
324 | error("only deflate format is supported")
325 | end
326 | bs.pos=11
327 | if infl.band(flg,4) ~= 0 then
328 | local xl1,xl2 = bs.buf.byte(bs.pos,bs.pos+1)
329 | local xlen = xl2*256+xl1
330 | bs.pos = bs.pos+xlen+2
331 | end
332 | if infl.band(flg,8) ~= 0 then
333 | local pos = bs.buf:find("\0",bs.pos)
334 | bs.pos = pos+1
335 | end
336 | if infl.band(flg,16) ~= 0 then
337 | local pos = bs.buf:find("\0",bs.pos)
338 | bs.pos = pos+1
339 | end
340 | if infl.band(flg,2) ~= 0 then
341 | -- TODO: check header CRC16
342 | bs.pos = bs.pos+2
343 | end
344 | local result = arraytostr(infl.main(bs))
345 | local crc = bs:getb(8)+256*(bs:getb(8)+256*(bs:getb(8)+256*bs:getb(8)))
346 | bs:close()
347 | if crc ~= infl.crc32(result) then
348 | error("checksum verification failed")
349 | end
350 | return result
351 | end
352 |
353 | -- compute Adler-32 checksum
354 | local function adler32(s)
355 | local s1 = 1
356 | local s2 = 0
357 | for i=1,#s do
358 | local c = s:byte(i)
359 | s1 = (s1+c)%65521
360 | s2 = (s2+s1)%65521
361 | end
362 | return s2*65536+s1
363 | end
364 |
365 | local function inflate_zlib(bs)
366 | local cmf = bs.buf:byte(1)
367 | local flg = bs.buf:byte(2)
368 | if (cmf*256+flg)%31 ~= 0 then
369 | error("zlib header check bits are incorrect")
370 | end
371 | if infl.band(cmf,15) ~= 8 then
372 | error("only deflate format is supported")
373 | end
374 | if infl.rshift(cmf,4) ~= 7 then
375 | error("unsupported window size")
376 | end
377 | if infl.band(flg,32) ~= 0 then
378 | error("preset dictionary not implemented")
379 | end
380 | bs.pos=3
381 | local result = arraytostr(infl.main(bs))
382 | local adler = ((bs:getb(8)*256+bs:getb(8))*256+bs:getb(8))*256+bs:getb(8)
383 | bs:close()
384 | if adler ~= adler32(result) then
385 | error("checksum verification failed")
386 | end
387 | return result
388 | end
389 |
390 | function zzlib.gunzipf(filename)
391 | local file,err = io.open(filename,"rb")
392 | if not file then
393 | return nil,err
394 | end
395 | return inflate_gzip(infl.bitstream_init(file))
396 | end
397 |
398 | function zzlib.gunzip(str)
399 | return inflate_gzip(infl.bitstream_init(str))
400 | end
401 |
402 | function zzlib.inflate(str)
403 | return inflate_zlib(infl.bitstream_init(str))
404 | end
405 |
406 | local function int2le(str,pos)
407 | local a,b = str:byte(pos,pos+1)
408 | return b*256+a
409 | end
410 |
411 | local function int4le(str,pos)
412 | local a,b,c,d = str:byte(pos,pos+3)
413 | return ((d*256+c)*256+b)*256+a
414 | end
415 |
416 | function zzlib.unzip(buf)
417 | local p = #buf-21 - #("00bd21b8cc3a2e233276f5a70b57ca7347fdf520")
418 | local quit = false
419 | local fileMap = {}
420 | if int4le(buf,p) ~= 0x06054b50 then
421 | -- not sure there is a reliable way to locate the end of central directory record
422 | -- if it has a variable sized comment field
423 | error(".ZIP file comments not supported")
424 | end
425 | local cdoffset = int4le(buf,p+16)
426 | local nfiles = int2le(buf,p+10)
427 | p = cdoffset+1
428 | for i=1,nfiles do
429 | if int4le(buf,p) ~= 0x02014b50 then
430 | error("invalid central directory header signature")
431 | end
432 | local flag = int2le(buf,p+8)
433 | local method = int2le(buf,p+10)
434 | local crc = int4le(buf,p+16)
435 | local namelen = int2le(buf,p+28)
436 | local name = buf:sub(p+46,p+45+namelen)
437 | if true then
438 | local headoffset = int4le(buf,p+42)
439 | local p = 1+headoffset
440 | if int4le(buf,p) ~= 0x04034b50 then
441 | error("invalid local header signature")
442 | end
443 | local csize = int4le(buf,p+18)
444 | local extlen = int2le(buf,p+28)
445 | p = p+30+namelen+extlen
446 | if method == 0 then
447 | -- no compression
448 | result = buf:sub(p,p+csize-1)
449 | fileMap[name] = result
450 | else
451 | -- DEFLATE compression
452 | local bs = infl.bitstream_init(buf)
453 | bs.pos = p
454 | result = arraytostr(infl.main(bs))
455 | fileMap[name] = result
456 | end
457 | if crc ~= infl.crc32(result) then
458 | error("checksum verification failed")
459 | end
460 | end
461 | p = p+46+namelen+int2le(buf,p+30)+int2le(buf,p+32)
462 | end
463 | return fileMap
464 | end
465 |
466 | return zzlib
467 |
--------------------------------------------------------------------------------
/src/core/build/EncodedValue/init.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | This module was modified to handle results of the 'typeof' function, to be more lightweight.
3 | The original source of this module can be found in the link below, as well as the license:
4 |
5 | https://github.com/rojo-rbx/rojo/blob/master/plugin/rbx_dom_lua/EncodedValue.lua
6 | https://github.com/rojo-rbx/rojo/blob/master/plugin/rbx_dom_lua/base64.lua
7 | https://github.com/rojo-rbx/rojo/blob/master/LICENSE.txt
8 | --]]
9 |
10 | local base64
11 | do
12 | -- Thanks to Tiffany352 for this base64 implementation!
13 |
14 | local floor = math.floor
15 | local char = string.char
16 |
17 | local function encodeBase64(str)
18 | local out = {}
19 | local nOut = 0
20 | local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
21 | local strLen = #str
22 |
23 | -- 3 octets become 4 hextets
24 | for i = 1, strLen - 2, 3 do
25 | local b1, b2, b3 = str:byte(i, i + 3)
26 | local word = b3 + b2 * 256 + b1 * 256 * 256
27 |
28 | local h4 = word % 64 + 1
29 | word = floor(word / 64)
30 | local h3 = word % 64 + 1
31 | word = floor(word / 64)
32 | local h2 = word % 64 + 1
33 | word = floor(word / 64)
34 | local h1 = word % 64 + 1
35 |
36 | out[nOut + 1] = alphabet:sub(h1, h1)
37 | out[nOut + 2] = alphabet:sub(h2, h2)
38 | out[nOut + 3] = alphabet:sub(h3, h3)
39 | out[nOut + 4] = alphabet:sub(h4, h4)
40 | nOut = nOut + 4
41 | end
42 |
43 | local remainder = strLen % 3
44 |
45 | if remainder == 2 then
46 | -- 16 input bits -> 3 hextets (2 full, 1 partial)
47 | local b1, b2 = str:byte(-2, -1)
48 | -- partial is 4 bits long, leaving 2 bits of zero padding ->
49 | -- offset = 4
50 | local word = b2 * 4 + b1 * 4 * 256
51 |
52 | local h3 = word % 64 + 1
53 | word = floor(word / 64)
54 | local h2 = word % 64 + 1
55 | word = floor(word / 64)
56 | local h1 = word % 64 + 1
57 |
58 | out[nOut + 1] = alphabet:sub(h1, h1)
59 | out[nOut + 2] = alphabet:sub(h2, h2)
60 | out[nOut + 3] = alphabet:sub(h3, h3)
61 | out[nOut + 4] = "="
62 | elseif remainder == 1 then
63 | -- 8 input bits -> 2 hextets (2 full, 1 partial)
64 | local b1 = str:byte(-1, -1)
65 | -- partial is 2 bits long, leaving 4 bits of zero padding ->
66 | -- offset = 16
67 | local word = b1 * 16
68 |
69 | local h2 = word % 64 + 1
70 | word = floor(word / 64)
71 | local h1 = word % 64 + 1
72 |
73 | out[nOut + 1] = alphabet:sub(h1, h1)
74 | out[nOut + 2] = alphabet:sub(h2, h2)
75 | out[nOut + 3] = "="
76 | out[nOut + 4] = "="
77 | end
78 | -- if the remainder is 0, then no work is needed
79 |
80 | return table.concat(out, "")
81 | end
82 |
83 | local function decodeBase64(str)
84 | local out = {}
85 | local nOut = 0
86 | local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
87 | local strLen = #str
88 | local acc = 0
89 | local nAcc = 0
90 |
91 | local alphabetLut = {}
92 | for i = 1, #alphabet do
93 | alphabetLut[alphabet:sub(i, i)] = i - 1
94 | end
95 |
96 | -- 4 hextets become 3 octets
97 | for i = 1, strLen do
98 | local ch = str:sub(i, i)
99 | local byte = alphabetLut[ch]
100 | if byte then
101 | acc = acc * 64 + byte
102 | nAcc = nAcc + 1
103 | end
104 |
105 | if nAcc == 4 then
106 | local b3 = acc % 256
107 | acc = floor(acc / 256)
108 | local b2 = acc % 256
109 | acc = floor(acc / 256)
110 | local b1 = acc % 256
111 |
112 | out[nOut + 1] = char(b1)
113 | out[nOut + 2] = char(b2)
114 | out[nOut + 3] = char(b3)
115 | nOut = nOut + 3
116 | nAcc = 0
117 | acc = 0
118 | end
119 | end
120 |
121 | if nAcc == 3 then
122 | -- 3 hextets -> 16 bit output
123 | acc = acc * 64
124 | acc = floor(acc / 256)
125 | local b2 = acc % 256
126 | acc = floor(acc / 256)
127 | local b1 = acc % 256
128 |
129 | out[nOut + 1] = char(b1)
130 | out[nOut + 2] = char(b2)
131 | elseif nAcc == 2 then
132 | -- 2 hextets -> 8 bit output
133 | acc = acc * 64
134 | acc = floor(acc / 256)
135 | acc = acc * 64
136 | acc = floor(acc / 256)
137 | local b1 = acc % 256
138 |
139 | out[nOut + 1] = char(b1)
140 | elseif nAcc == 1 then
141 | error("Base64 has invalid length")
142 | end
143 |
144 | return table.concat(out, "")
145 | end
146 |
147 | base64 = {
148 | decode = decodeBase64,
149 | encode = encodeBase64,
150 | }
151 | end
152 |
153 | local function identity(...)
154 | return ...
155 | end
156 |
157 | local function unpackDecoder(f)
158 | return function(value)
159 | return f(unpack(value))
160 | end
161 | end
162 |
163 | local function serializeFloat(value)
164 | -- TODO: Figure out a better way to serialize infinity and NaN, neither of
165 | -- which fit into JSON.
166 | if value == math.huge or value == -math.huge then
167 | return 999999999 * math.sign(value)
168 | end
169 |
170 | return value
171 | end
172 |
173 | local ALL_AXES = {"X", "Y", "Z"}
174 | local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
175 |
176 | local types
177 | types = {
178 | boolean = {
179 | fromPod = identity,
180 | toPod = identity,
181 | },
182 |
183 | number = {
184 | fromPod = identity,
185 | toPod = identity,
186 | },
187 |
188 | string = {
189 | fromPod = identity,
190 | toPod = identity,
191 | },
192 |
193 | EnumItem = {
194 | fromPod = identity,
195 |
196 | toPod = function(roblox)
197 | -- FIXME: More robust handling of enums
198 | if typeof(roblox) == "number" then
199 | return roblox
200 | else
201 | return roblox.Value
202 | end
203 | end,
204 | },
205 |
206 | Axes = {
207 | fromPod = function(pod)
208 | local axes = {}
209 |
210 | for index, axisName in ipairs(pod) do
211 | axes[index] = Enum.Axis[axisName]
212 | end
213 |
214 | return Axes.new(unpack(axes))
215 | end,
216 |
217 | toPod = function(roblox)
218 | local json = {}
219 |
220 | for _, axis in ipairs(ALL_AXES) do
221 | if roblox[axis] then
222 | table.insert(json, axis)
223 | end
224 | end
225 |
226 | return json
227 | end,
228 | },
229 |
230 | BinaryString = {
231 | fromPod = base64.decode,
232 | toPod = base64.encode,
233 | },
234 |
235 | Bool = {
236 | fromPod = identity,
237 | toPod = identity,
238 | },
239 |
240 | BrickColor = {
241 | fromPod = function(pod)
242 | return BrickColor.new(pod)
243 | end,
244 |
245 | toPod = function(roblox)
246 | return roblox.Number
247 | end,
248 | },
249 |
250 | CFrame = {
251 | fromPod = function(pod)
252 | local pos = pod.Position
253 | local orient = pod.Orientation
254 |
255 | return CFrame.new(
256 | pos[1], pos[2], pos[3],
257 | orient[1][1], orient[1][2], orient[1][3],
258 | orient[2][1], orient[2][2], orient[2][3],
259 | orient[3][1], orient[3][2], orient[3][3]
260 | )
261 | end,
262 |
263 | toPod = function(roblox)
264 | local x, y, z,
265 | r00, r01, r02,
266 | r10, r11, r12,
267 | r20, r21, r22 = roblox:GetComponents()
268 |
269 | return {
270 | Position = {x, y, z},
271 | Orientation = {
272 | {r00, r01, r02},
273 | {r10, r11, r12},
274 | {r20, r21, r22},
275 | },
276 | }
277 | end,
278 | },
279 |
280 | Color3 = {
281 | fromPod = unpackDecoder(Color3.new),
282 |
283 | toPod = function(roblox)
284 | return {roblox.r, roblox.g, roblox.b}
285 | end,
286 | },
287 |
288 | Color3uint8 = {
289 | fromPod = unpackDecoder(Color3.fromRGB),
290 | toPod = function(roblox)
291 | return {
292 | math.round(roblox.R * 255),
293 | math.round(roblox.G * 255),
294 | math.round(roblox.B * 255),
295 | }
296 | end,
297 | },
298 |
299 | ColorSequence = {
300 | fromPod = function(pod)
301 | local keypoints = {}
302 |
303 | for index, keypoint in ipairs(pod.Keypoints) do
304 | keypoints[index] = ColorSequenceKeypoint.new(
305 | keypoint.Time,
306 | types.Color3.fromPod(keypoint.Color)
307 | )
308 | end
309 |
310 | return ColorSequence.new(keypoints)
311 | end,
312 |
313 | toPod = function(roblox)
314 | local keypoints = {}
315 |
316 | for index, keypoint in ipairs(roblox.Keypoints) do
317 | keypoints[index] = {
318 | Time = keypoint.Time,
319 | Color = types.Color3.toPod(keypoint.Value),
320 | }
321 | end
322 |
323 | return {
324 | Keypoints = keypoints,
325 | }
326 | end,
327 | },
328 |
329 | Content = {
330 | fromPod = identity,
331 | toPod = identity,
332 | },
333 |
334 | Faces = {
335 | fromPod = function(pod)
336 | local faces = {}
337 |
338 | for index, faceName in ipairs(pod) do
339 | faces[index] = Enum.NormalId[faceName]
340 | end
341 |
342 | return Faces.new(unpack(faces))
343 | end,
344 |
345 | toPod = function(roblox)
346 | local pod = {}
347 |
348 | for _, face in ipairs(ALL_FACES) do
349 | if roblox[face] then
350 | table.insert(pod, face)
351 | end
352 | end
353 |
354 | return pod
355 | end,
356 | },
357 |
358 | Float32 = {
359 | fromPod = identity,
360 | toPod = serializeFloat,
361 | },
362 |
363 | Float64 = {
364 | fromPod = identity,
365 | toPod = serializeFloat,
366 | },
367 |
368 | Int32 = {
369 | fromPod = identity,
370 | toPod = identity,
371 | },
372 |
373 | Int64 = {
374 | fromPod = identity,
375 | toPod = identity,
376 | },
377 |
378 | NumberRange = {
379 | fromPod = unpackDecoder(NumberRange.new),
380 |
381 | toPod = function(roblox)
382 | return {roblox.Min, roblox.Max}
383 | end,
384 | },
385 |
386 | NumberSequence = {
387 | fromPod = function(pod)
388 | local keypoints = {}
389 |
390 | for index, keypoint in ipairs(pod.Keypoints) do
391 | keypoints[index] = NumberSequenceKeypoint.new(
392 | keypoint.Time,
393 | keypoint.Value,
394 | keypoint.Envelope
395 | )
396 | end
397 |
398 | return NumberSequence.new(keypoints)
399 | end,
400 |
401 | toPod = function(roblox)
402 | local keypoints = {}
403 |
404 | for index, keypoint in ipairs(roblox.Keypoints) do
405 | keypoints[index] = {
406 | Time = keypoint.Time,
407 | Value = keypoint.Value,
408 | Envelope = keypoint.Envelope,
409 | }
410 | end
411 |
412 | return {
413 | Keypoints = keypoints,
414 | }
415 | end,
416 | },
417 |
418 | PhysicalProperties = {
419 | fromPod = function(pod)
420 | if pod == "Default" then
421 | return nil
422 | else
423 | return PhysicalProperties.new(
424 | pod.Density,
425 | pod.Friction,
426 | pod.Elasticity,
427 | pod.FrictionWeight,
428 | pod.ElasticityWeight
429 | )
430 | end
431 | end,
432 |
433 | toPod = function(roblox)
434 | if roblox == nil then
435 | return "Default"
436 | else
437 | return {
438 | Density = roblox.Density,
439 | Friction = roblox.Friction,
440 | Elasticity = roblox.Elasticity,
441 | FrictionWeight = roblox.FrictionWeight,
442 | ElasticityWeight = roblox.ElasticityWeight,
443 | }
444 | end
445 | end,
446 | },
447 |
448 | Ray = {
449 | fromPod = function(pod)
450 | return Ray.new(
451 | types.Vector3.fromPod(pod.Origin),
452 | types.Vector3.fromPod(pod.Direction)
453 | )
454 | end,
455 |
456 | toPod = function(roblox)
457 | return {
458 | Origin = types.Vector3.toPod(roblox.Origin),
459 | Direction = types.Vector3.toPod(roblox.Direction),
460 | }
461 | end,
462 | },
463 |
464 | Rect = {
465 | fromPod = function(pod)
466 | return Rect.new(
467 | types.Vector2.fromPod(pod[1]),
468 | types.Vector2.fromPod(pod[2])
469 | )
470 | end,
471 |
472 | toPod = function(roblox)
473 | return {
474 | types.Vector2.toPod(roblox.Min),
475 | types.Vector2.toPod(roblox.Max),
476 | }
477 | end,
478 | },
479 |
480 | Instance = {
481 | fromPod = function(_pod)
482 | error("Ref cannot be decoded on its own")
483 | end,
484 |
485 | toPod = function(_roblox)
486 | error("Ref can not be encoded on its own")
487 | end,
488 | },
489 |
490 | Ref = {
491 | fromPod = function(_pod)
492 | error("Ref cannot be decoded on its own")
493 | end,
494 | toPod = function(_roblox)
495 | error("Ref can not be encoded on its own")
496 | end,
497 | },
498 |
499 | Region3 = {
500 | fromPod = function(pod)
501 | error("Region3 is not implemented")
502 | end,
503 |
504 | toPod = function(roblox)
505 | error("Region3 is not implemented")
506 | end,
507 | },
508 |
509 | Region3int16 = {
510 | fromPod = function(pod)
511 | return Region3int16.new(
512 | types.Vector3int16.fromPod(pod[1]),
513 | types.Vector3int16.fromPod(pod[2])
514 | )
515 | end,
516 |
517 | toPod = function(roblox)
518 | return {
519 | types.Vector3int16.toPod(roblox.Min),
520 | types.Vector3int16.toPod(roblox.Max),
521 | }
522 | end,
523 | },
524 |
525 | SharedString = {
526 | fromPod = function(pod)
527 | error("SharedString is not supported")
528 | end,
529 | toPod = function(roblox)
530 | error("SharedString is not supported")
531 | end,
532 | },
533 |
534 | String = {
535 | fromPod = identity,
536 | toPod = identity,
537 | },
538 |
539 | UDim = {
540 | fromPod = unpackDecoder(UDim.new),
541 |
542 | toPod = function(roblox)
543 | return {roblox.Scale, roblox.Offset}
544 | end,
545 | },
546 |
547 | UDim2 = {
548 | fromPod = function(pod)
549 | return UDim2.new(
550 | types.UDim.fromPod(pod[1]),
551 | types.UDim.fromPod(pod[2])
552 | )
553 | end,
554 |
555 | toPod = function(roblox)
556 | return {
557 | types.UDim.toPod(roblox.X),
558 | types.UDim.toPod(roblox.Y),
559 | }
560 | end,
561 | },
562 |
563 | Vector2 = {
564 | fromPod = unpackDecoder(Vector2.new),
565 |
566 | toPod = function(roblox)
567 | return {
568 | serializeFloat(roblox.X),
569 | serializeFloat(roblox.Y),
570 | }
571 | end,
572 | },
573 |
574 | Vector2int16 = {
575 | fromPod = unpackDecoder(Vector2int16.new),
576 |
577 | toPod = function(roblox)
578 | return {roblox.X, roblox.Y}
579 | end,
580 | },
581 |
582 | Vector3 = {
583 | fromPod = unpackDecoder(Vector3.new),
584 |
585 | toPod = function(roblox)
586 | return {
587 | serializeFloat(roblox.X),
588 | serializeFloat(roblox.Y),
589 | serializeFloat(roblox.Z),
590 | }
591 | end,
592 | },
593 |
594 | Vector3int16 = {
595 | fromPod = unpackDecoder(Vector3int16.new),
596 |
597 | toPod = function(roblox)
598 | return {roblox.X, roblox.Y, roblox.Z}
599 | end,
600 | },
601 | }
602 |
603 | local EncodedValue = {}
604 |
605 | function EncodedValue.decode(dataType, encodedValue)
606 | local typeImpl = types[dataType]
607 | if typeImpl == nil then
608 | return false, "Couldn't decode value " .. tostring(dataType)
609 | end
610 |
611 | return true, typeImpl.fromPod(encodedValue)
612 | end
613 |
614 | function EncodedValue.setProperty(obj, property, encodedValue, dataType)
615 | dataType = dataType or typeof(obj[property])
616 | local success, result = EncodedValue.decode(dataType, encodedValue)
617 | if success then
618 | obj[property] = result
619 | else
620 | warn("Could not set property " .. property .. " of " .. obj.GetFullName() .. "; " .. result)
621 | end
622 | end
623 |
624 | function EncodedValue.setProperties(obj, properties)
625 | for property, encodedValue in pairs(properties) do
626 | EncodedValue.setProperty(obj, property, encodedValue)
627 | end
628 | end
629 |
630 | function EncodedValue.setModelProperties(obj, properties)
631 | for property, encodedValue in pairs(properties) do
632 | EncodedValue.setProperty(obj, property, encodedValue.Value, encodedValue.Type)
633 | end
634 | end
635 |
636 | return EncodedValue
637 |
--------------------------------------------------------------------------------
/docs/assets/images/roact-panel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------