rowork 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/bin/rowork.js +18 -0
  4. package/dashboard/app.css +234 -0
  5. package/dashboard/app.js +117 -0
  6. package/dashboard/index.html +67 -0
  7. package/dist/cli/errors.d.ts +15 -0
  8. package/dist/cli/errors.js +18 -0
  9. package/dist/cli/program.d.ts +19 -0
  10. package/dist/cli/program.js +59 -0
  11. package/dist/cli/registry.d.ts +37 -0
  12. package/dist/cli/registry.js +61 -0
  13. package/dist/commands/add.d.ts +8 -0
  14. package/dist/commands/add.js +77 -0
  15. package/dist/commands/assets-setup.d.ts +18 -0
  16. package/dist/commands/assets-setup.js +136 -0
  17. package/dist/commands/assets.d.ts +2 -0
  18. package/dist/commands/assets.js +153 -0
  19. package/dist/commands/console.d.ts +4 -0
  20. package/dist/commands/console.js +141 -0
  21. package/dist/commands/dashboard.d.ts +2 -0
  22. package/dist/commands/dashboard.js +50 -0
  23. package/dist/commands/dev-background.d.ts +3 -0
  24. package/dist/commands/dev-background.js +87 -0
  25. package/dist/commands/dev.d.ts +2 -0
  26. package/dist/commands/dev.js +270 -0
  27. package/dist/commands/eject.d.ts +2 -0
  28. package/dist/commands/eject.js +136 -0
  29. package/dist/commands/index.d.ts +8 -0
  30. package/dist/commands/index.js +54 -0
  31. package/dist/commands/info.d.ts +3 -0
  32. package/dist/commands/info.js +50 -0
  33. package/dist/commands/init.d.ts +2 -0
  34. package/dist/commands/init.js +55 -0
  35. package/dist/commands/links.d.ts +32 -0
  36. package/dist/commands/links.js +152 -0
  37. package/dist/commands/make-event.d.ts +2 -0
  38. package/dist/commands/make-event.js +160 -0
  39. package/dist/commands/make-menu.d.ts +3 -0
  40. package/dist/commands/make-menu.js +36 -0
  41. package/dist/commands/make-stat.d.ts +2 -0
  42. package/dist/commands/make-stat.js +199 -0
  43. package/dist/commands/make-tool.d.ts +2 -0
  44. package/dist/commands/make-tool.js +124 -0
  45. package/dist/commands/make-ui.d.ts +3 -0
  46. package/dist/commands/make-ui.js +199 -0
  47. package/dist/commands/make.d.ts +21 -0
  48. package/dist/commands/make.js +181 -0
  49. package/dist/commands/next-steps.d.ts +5 -0
  50. package/dist/commands/next-steps.js +18 -0
  51. package/dist/commands/start.d.ts +2 -0
  52. package/dist/commands/start.js +144 -0
  53. package/dist/commands/studio.d.ts +3 -0
  54. package/dist/commands/studio.js +45 -0
  55. package/dist/commands/update.d.ts +2 -0
  56. package/dist/commands/update.js +173 -0
  57. package/dist/commands/wire.d.ts +2 -0
  58. package/dist/commands/wire.js +34 -0
  59. package/dist/core/agent-docs.d.ts +23 -0
  60. package/dist/core/agent-docs.js +151 -0
  61. package/dist/core/assets.d.ts +96 -0
  62. package/dist/core/assets.js +268 -0
  63. package/dist/core/background.d.ts +47 -0
  64. package/dist/core/background.js +149 -0
  65. package/dist/core/config.d.ts +9 -0
  66. package/dist/core/config.js +62 -0
  67. package/dist/core/exec.d.ts +22 -0
  68. package/dist/core/exec.js +38 -0
  69. package/dist/core/format-generated.d.ts +12 -0
  70. package/dist/core/format-generated.js +28 -0
  71. package/dist/core/generate.d.ts +37 -0
  72. package/dist/core/generate.js +94 -0
  73. package/dist/core/github-release.d.ts +28 -0
  74. package/dist/core/github-release.js +38 -0
  75. package/dist/core/info.d.ts +77 -0
  76. package/dist/core/info.js +65 -0
  77. package/dist/core/modules.d.ts +32 -0
  78. package/dist/core/modules.js +265 -0
  79. package/dist/core/naming.d.ts +6 -0
  80. package/dist/core/naming.js +26 -0
  81. package/dist/core/open-cloud.d.ts +58 -0
  82. package/dist/core/open-cloud.js +170 -0
  83. package/dist/core/project-index.d.ts +26 -0
  84. package/dist/core/project-index.js +59 -0
  85. package/dist/core/rojo-edit.d.ts +18 -0
  86. package/dist/core/rojo-edit.js +53 -0
  87. package/dist/core/rokit-installer.d.ts +15 -0
  88. package/dist/core/rokit-installer.js +77 -0
  89. package/dist/core/scaffold.d.ts +44 -0
  90. package/dist/core/scaffold.js +163 -0
  91. package/dist/core/schema-edit.d.ts +46 -0
  92. package/dist/core/schema-edit.js +125 -0
  93. package/dist/core/source-scan.d.ts +24 -0
  94. package/dist/core/source-scan.js +84 -0
  95. package/dist/core/studio.d.ts +83 -0
  96. package/dist/core/studio.js +225 -0
  97. package/dist/core/toolchain.d.ts +31 -0
  98. package/dist/core/toolchain.js +98 -0
  99. package/dist/core/tsconfig-edit.d.ts +11 -0
  100. package/dist/core/tsconfig-edit.js +47 -0
  101. package/dist/core/ui-edit.d.ts +31 -0
  102. package/dist/core/ui-edit.js +89 -0
  103. package/dist/core/versions.d.ts +24 -0
  104. package/dist/core/versions.js +67 -0
  105. package/dist/dashboard/open.d.ts +3 -0
  106. package/dist/dashboard/open.js +18 -0
  107. package/dist/dashboard/server.d.ts +28 -0
  108. package/dist/dashboard/server.js +191 -0
  109. package/dist/index.d.ts +3 -0
  110. package/dist/index.js +3 -0
  111. package/dist/main.d.ts +9 -0
  112. package/dist/main.js +135 -0
  113. package/dist/modules/format.d.ts +3 -0
  114. package/dist/modules/format.js +28 -0
  115. package/dist/modules/index.d.ts +7 -0
  116. package/dist/modules/index.js +12 -0
  117. package/dist/modules/integrations.d.ts +8 -0
  118. package/dist/modules/integrations.js +54 -0
  119. package/dist/modules/leaderstats.d.ts +3 -0
  120. package/dist/modules/leaderstats.js +104 -0
  121. package/dist/modules/lint.d.ts +5 -0
  122. package/dist/modules/lint.js +43 -0
  123. package/dist/modules/networking.d.ts +23 -0
  124. package/dist/modules/networking.js +246 -0
  125. package/dist/modules/player-data.d.ts +19 -0
  126. package/dist/modules/player-data.js +176 -0
  127. package/dist/modules/types.d.ts +96 -0
  128. package/dist/modules/types.js +2 -0
  129. package/dist/modules/ui.d.ts +3 -0
  130. package/dist/modules/ui.js +67 -0
  131. package/dist/plugins/api.d.ts +127 -0
  132. package/dist/plugins/api.js +21 -0
  133. package/dist/plugins/loader.d.ts +20 -0
  134. package/dist/plugins/loader.js +128 -0
  135. package/dist/process/log-mux.d.ts +13 -0
  136. package/dist/process/log-mux.js +31 -0
  137. package/dist/process/supervisor.d.ts +48 -0
  138. package/dist/process/supervisor.js +141 -0
  139. package/dist/process/tree-kill.d.ts +25 -0
  140. package/dist/process/tree-kill.js +70 -0
  141. package/dist/templates/engine.d.ts +18 -0
  142. package/dist/templates/engine.js +54 -0
  143. package/dist/ui/logger.d.ts +5 -0
  144. package/dist/ui/logger.js +53 -0
  145. package/dist/ui/prompt.d.ts +9 -0
  146. package/dist/ui/prompt.js +24 -0
  147. package/package.json +69 -0
  148. package/templates/init/README.md.tmpl +37 -0
  149. package/templates/init/_gitignore.tmpl +23 -0
  150. package/templates/init/default.project.json.tmpl +57 -0
  151. package/templates/init/package.json.tmpl +12 -0
  152. package/templates/init/rokit.toml.tmpl +5 -0
  153. package/templates/init/src/client/controllers/ExampleController.ts.tmpl +12 -0
  154. package/templates/init/src/client/runtime.client.ts.tmpl +6 -0
  155. package/templates/init/src/server/runtime.server.ts.tmpl +12 -0
  156. package/templates/init/src/server/services/ExampleService.ts.tmpl +12 -0
  157. package/templates/init/src/shared/_gitkeep.tmpl +0 -0
  158. package/templates/init/tsconfig.json.tmpl +28 -0
  159. package/templates/integrations/data-replication/DataReplicationService.ts.tmpl +33 -0
  160. package/templates/integrations/data-replication/PlayerDataController.ts.tmpl +40 -0
  161. package/templates/integrations/data-replication/dataEvents.ts.tmpl +21 -0
  162. package/templates/make/component.ts.tmpl +8 -0
  163. package/templates/make/controller.ts.tmpl +6 -0
  164. package/templates/make/event-handler.ts.tmpl +22 -0
  165. package/templates/make/screen.tsx.tmpl +35 -0
  166. package/templates/make/service.ts.tmpl +6 -0
  167. package/templates/make/stat-service.ts.tmpl +26 -0
  168. package/templates/make/ui-button.tsx.tmpl +22 -0
  169. package/templates/make/ui-image.tsx.tmpl +11 -0
  170. package/templates/make/ui-label.tsx.tmpl +18 -0
  171. package/templates/make/ui-panel.tsx.tmpl +33 -0
  172. package/templates/make/ui-story.tsx.tmpl +13 -0
  173. package/templates/modules/format/prettierignore.tmpl +9 -0
  174. package/templates/modules/format/prettierrc.json.tmpl +4 -0
  175. package/templates/modules/leaderstats/LeaderstatsService.ts.tmpl +69 -0
  176. package/templates/modules/lint/eslint.config.mjs.tmpl +17 -0
  177. package/templates/modules/networking/client-network.ts.tmpl +9 -0
  178. package/templates/modules/networking/networking.ts.tmpl +22 -0
  179. package/templates/modules/networking/rate-limit.ts.tmpl +52 -0
  180. package/templates/modules/networking/server-network.ts.tmpl +20 -0
  181. package/templates/modules/player-data/PlayerData.ts.tmpl +15 -0
  182. package/templates/modules/player-data/PlayerDataService.ts.tmpl +127 -0
  183. package/templates/modules/ui/App.tsx.tmpl +19 -0
  184. package/templates/modules/ui/Button.story.tsx.tmpl +13 -0
  185. package/templates/modules/ui/Button.tsx.tmpl +21 -0
  186. package/templates/modules/ui/UiController.tsx.tmpl +21 -0
  187. package/templates/modules/ui/screens/HomeScreen.tsx.tmpl +20 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowSyntheticDefaultImports": true,
4
+ "downlevelIteration": true,
5
+ "jsx": "react",
6
+ "jsxFactory": "React.createElement",
7
+ "jsxFragmentFactory": "React.Fragment",
8
+ "module": "commonjs",
9
+ "moduleResolution": "Node",
10
+ "noLib": true,
11
+ "resolveJsonModule": true,
12
+ "experimentalDecorators": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "moduleDetection": "force",
15
+ "strict": true,
16
+ "target": "ESNext",
17
+ "typeRoots": ["node_modules/@rbxts", "node_modules/@flamework"],
18
+
19
+ "rootDir": "src",
20
+ "outDir": "out",
21
+ "baseUrl": "src",
22
+ "incremental": true,
23
+ "tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
24
+ "declaration": false,
25
+
26
+ "plugins": [{ "transform": "rbxts-transformer-flamework" }]
27
+ }
28
+ }
@@ -0,0 +1,33 @@
1
+ import { OnStart, Service } from "@flamework/core";
2
+ import { DataEvents } from "{{ eventsImport }}";
3
+ import { limit } from "{{ rateLimitImport }}";
4
+ import { PlayerDataService } from "./PlayerDataService";
5
+
6
+ const Events = DataEvents.createServer({
7
+ middleware: {
8
+ // A client asks for its data once when it starts: more than that is not normal.
9
+ dataRequested: [limit(5)],
10
+ },
11
+ });
12
+
13
+ /**
14
+ * Sends each player's data to their own client, and only theirs: one player
15
+ * never sees another's. The data is a copy for display: the server stays the
16
+ * only place that changes it (PlayerDataService.update).
17
+ */
18
+ @Service()
19
+ export class DataReplicationService implements OnStart {
20
+ constructor(private readonly playerData: PlayerDataService) {}
21
+
22
+ onStart(): void {
23
+ this.playerData.onLoaded((player, data) => Events.dataChanged.fire(player, data));
24
+ this.playerData.onChanged((player, data) => Events.dataChanged.fire(player, data));
25
+
26
+ // A client can start after its data loaded, and would never hear about it
27
+ // otherwise: it asks once when it is ready.
28
+ Events.dataRequested.connect((player) => {
29
+ const data = this.playerData.get(player);
30
+ if (data !== undefined) Events.dataChanged.fire(player, data);
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,40 @@
1
+ import { Controller, OnStart } from "@flamework/core";
2
+ import type { PlayerData } from "{{ dataImport }}";
3
+ import { DataEvents } from "{{ eventsImport }}";
4
+
5
+ const Events = DataEvents.createClient({});
6
+
7
+ /**
8
+ * This player's data on their own screen, for interfaces to display. It is a
9
+ * read-only copy sent by the server: to change data, ask the server (a message,
10
+ * then PlayerDataService.update there). Changing it here changes nothing.
11
+ *
12
+ * constructor(private readonly playerData: PlayerDataController) {}
13
+ * this.playerData.onChanged((data) => label.Text = `${data.coins}`);
14
+ */
15
+ @Controller()
16
+ export class PlayerDataController implements OnStart {
17
+ private data: PlayerData | undefined;
18
+ private readonly listeners = new Array<(data: PlayerData) => void>();
19
+
20
+ onStart(): void {
21
+ Events.dataChanged.connect((data) => {
22
+ this.data = data;
23
+ for (const listener of this.listeners) task.spawn(listener, data);
24
+ });
25
+
26
+ // Ask the server for the data in case it loaded before this client was ready.
27
+ Events.dataRequested.fire();
28
+ }
29
+
30
+ /** The data, or undefined until the server has sent it. */
31
+ get(): Readonly<PlayerData> | undefined {
32
+ return this.data;
33
+ }
34
+
35
+ /** Runs each time the data changes, and right away if it is already known. */
36
+ onChanged(callback: (data: PlayerData) => void): void {
37
+ this.listeners.push(callback);
38
+ if (this.data !== undefined) task.spawn(callback, this.data);
39
+ }
40
+ }
@@ -0,0 +1,21 @@
1
+ import { Networking } from "@flamework/networking";
2
+ import type { PlayerData } from "{{ dataImport }}";
3
+
4
+ /**
5
+ * How a player's saved data reaches their own screen. Generated because both
6
+ * the player-data and the networking modules are installed.
7
+ *
8
+ * It has its own events on purpose, so your own src/shared/networking.ts stays
9
+ * yours: nothing here edits it.
10
+ */
11
+ interface ClientToServerEvents {
12
+ /** The client has started and wants its data. It may have loaded before the client was ready. */
13
+ dataRequested(): void;
14
+ }
15
+
16
+ interface ServerToClientEvents {
17
+ /** The player's data: sent when it loads and each time it changes. */
18
+ dataChanged(data: PlayerData): void;
19
+ }
20
+
21
+ export const DataEvents = Networking.createEvent<ClientToServerEvents, ServerToClientEvents>();
@@ -0,0 +1,8 @@
1
+ import { BaseComponent, Component } from "@flamework/components";
2
+ import { OnStart } from "@flamework/core";
3
+
4
+ /** Attached by Flamework to every instance carrying the "{{ tag }}" CollectionService tag. */
5
+ @Component({ tag: "{{ tag }}" })
6
+ export class {{ className }} extends BaseComponent implements OnStart {
7
+ onStart(): void {}
8
+ }
@@ -0,0 +1,6 @@
1
+ import { Controller, OnStart } from "@flamework/core";
2
+
3
+ @Controller()
4
+ export class {{ className }} implements OnStart {
5
+ onStart(): void {}
6
+ }
@@ -0,0 +1,22 @@
1
+ import { OnStart, Service } from "@flamework/core";
2
+ import { Events } from "{{ networkImport }}";
3
+ {{ imports }}
4
+
5
+ /**
6
+ * Runs on the server each time a player's client sends `{{ event }}`.
7
+ *
8
+ * Everything a client sends can be forged: a player can send any value, whenever
9
+ * they like, as often as they like. So decide HERE what happens and by how much,
10
+ * and never copy something you received into saved data.
11
+ */
12
+ @Service()
13
+ export class {{ className }} implements OnStart {
14
+ constructor({{ ctor }}) {}
15
+
16
+ onStart(): void {
17
+ Events.{{ event }}.connect((player) => {
18
+ // TODO: check that this player may do this now (cooldown, cost, distance...).
19
+ {{ body }}
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,35 @@
1
+ import React from "@rbxts/react";
2
+
3
+ /**
4
+ * The {{ name }} screen.
5
+ *
6
+ * A screen is a ScreenGui: one full-screen layer of your interface. `App` shows it
7
+ * (Rowork listed it there for you), and each screen is separate so it can be shown,
8
+ * hidden and ordered on its own.
9
+ *
10
+ * - `key` is the name the ScreenGui gets in the game (PlayerGui > {{ name }}), so scripts can find it.
11
+ * - `ResetOnSpawn={false}` keeps it when the character respawns.
12
+ * - `DisplayOrder` (add it to the ScreenGui) puts a screen in front of another: higher is in front.
13
+ * - `Enabled` shows or hides the whole screen, e.g. from a `useState` in this component.
14
+ *
15
+ * Add elements with `rowork make:ui <name> --in {{ name }}`, or write them between the frame tags.
16
+ */
17
+ export function {{ className }}() {
18
+ return (
19
+ <screengui key="{{ name }}" ResetOnSpawn={false} ZIndexBehavior={Enum.ZIndexBehavior.Sibling}>
20
+ <frame Size={UDim2.fromScale(1, 1)} BackgroundTransparency={1}>
21
+ {/* A placeholder, so you can see the screen work: delete it once you add your own. */}
22
+ <textlabel
23
+ Size={UDim2.fromOffset(300, 50)}
24
+ Position={UDim2.fromScale(0.5, 0.1)}
25
+ AnchorPoint={new Vector2(0.5, 0)}
26
+ BackgroundColor3={Color3.fromRGB(30, 30, 35)}
27
+ TextColor3={Color3.fromRGB(255, 255, 255)}
28
+ Text="{{ name }}"
29
+ TextScaled
30
+ />
31
+ {{ elementsMarker }}
32
+ </frame>
33
+ </screengui>
34
+ );
35
+ }
@@ -0,0 +1,6 @@
1
+ import { OnStart, Service } from "@flamework/core";
2
+ {{ imports }}
3
+ @Service()
4
+ export class {{ className }} implements OnStart {
5
+ {{ members }} onStart(): void {}
6
+ }
@@ -0,0 +1,26 @@
1
+ import { Service } from "@flamework/core";
2
+ import { PlayerDataService } from "./PlayerDataService";
3
+
4
+ /**
5
+ * Reads and changes the `{{ name }}` value that is saved for each player.
6
+ * The value itself lives in PlayerData (it was added there for you), so it is
7
+ * saved and loaded with everything else: nothing more to wire.
8
+ */
9
+ @Service()
10
+ export class {{ className }} {
11
+ constructor(private readonly playerData: PlayerDataService) {}
12
+
13
+ /** The player's `{{ name }}`, or undefined while their data is still loading. */
14
+ get(player: Player): {{ type }} | undefined {
15
+ const data = this.playerData.get(player);
16
+ return data === undefined ? undefined : data.{{ name }};
17
+ }
18
+
19
+ /** Sets the player's `{{ name }}`. */
20
+ set(player: Player, value: {{ type }}): void {
21
+ this.playerData.update(player, (data) => {
22
+ data.{{ name }} = value;
23
+ return data;
24
+ });
25
+ }
26
+ {{ extra }}}
@@ -0,0 +1,22 @@
1
+ import React from "@rbxts/react";
2
+
3
+ interface {{ name }}Props {
4
+ text?: string;
5
+ onClick?: () => void;
6
+ }
7
+
8
+ /**
9
+ * A button. `text` is what it says and `onClick` runs when it is pressed. Both are
10
+ * optional so `<{{ name }} />` works as soon as it is placed; a game rule never goes
11
+ * here (the client can be tampered with): send an event and let the server decide.
12
+ */
13
+ export function {{ name }}({ text = "{{ name }}", onClick }: {{ name }}Props) {
14
+ return (
15
+ <textbutton
16
+ Size={UDim2.fromOffset(200, 50)}
17
+ Text={text}
18
+ TextScaled
19
+ Event={{ Activated: () => onClick?.() }}
20
+ />
21
+ );
22
+ }
@@ -0,0 +1,11 @@
1
+ import React from "@rbxts/react";
2
+
3
+ interface {{ name }}Props {
4
+ /** An image id, e.g. `Assets.logo` from `rowork assets` (see src/shared/assets.ts). */
5
+ image?: string;
6
+ }
7
+
8
+ /** An image. Upload files with `rowork assets` and pass `Assets.<name>` as `image`. */
9
+ export function {{ name }}({ image = "" }: {{ name }}Props) {
10
+ return <imagelabel Image={image} Size={UDim2.fromOffset(120, 120)} BackgroundTransparency={1} />;
11
+ }
@@ -0,0 +1,18 @@
1
+ import React from "@rbxts/react";
2
+
3
+ interface {{ name }}Props {
4
+ text?: string;
5
+ }
6
+
7
+ /** A piece of text. `text` is optional so `<{{ name }} />` works as soon as it is placed. */
8
+ export function {{ name }}({ text = "{{ name }}" }: {{ name }}Props) {
9
+ return (
10
+ <textlabel
11
+ Size={UDim2.fromOffset(200, 30)}
12
+ BackgroundTransparency={1}
13
+ TextColor3={Color3.fromRGB(255, 255, 255)}
14
+ Text={text}
15
+ TextScaled
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,33 @@
1
+ import React from "@rbxts/react";
2
+
3
+ interface {{ name }}Props {
4
+ title?: string;
5
+ children?: React.ReactNode;
6
+ }
7
+
8
+ /**
9
+ * A rounded panel with a title. Whatever you put between `<{{ name }}>` and `</{{ name }}>`
10
+ * is stacked below the title (that is what `children` is), so a panel can hold anything.
11
+ */
12
+ export function {{ name }}({ title = "{{ name }}", children }: {{ name }}Props) {
13
+ return (
14
+ <frame Size={UDim2.fromOffset(300, 200)} BackgroundColor3={Color3.fromRGB(30, 30, 35)}>
15
+ <uicorner CornerRadius={new UDim(0, 8)} />
16
+ <uipadding
17
+ PaddingTop={new UDim(0, 10)}
18
+ PaddingBottom={new UDim(0, 10)}
19
+ PaddingLeft={new UDim(0, 10)}
20
+ PaddingRight={new UDim(0, 10)}
21
+ />
22
+ <uilistlayout Padding={new UDim(0, 8)} SortOrder={Enum.SortOrder.LayoutOrder} />
23
+ <textlabel
24
+ Size={new UDim2(1, 0, 0, 30)}
25
+ BackgroundTransparency={1}
26
+ TextColor3={Color3.fromRGB(255, 255, 255)}
27
+ Text={title}
28
+ TextScaled
29
+ />
30
+ {children}
31
+ </frame>
32
+ );
33
+ }
@@ -0,0 +1,13 @@
1
+ import React from "@rbxts/react";
2
+ import ReactRoblox from "@rbxts/react-roblox";
3
+ import { CreateReactStory } from "@rbxts/ui-labs";
4
+ import { {{ name }} } from "./{{ name }}";
5
+
6
+ /**
7
+ * A story shows one component on its own, inside Studio, without running the game
8
+ * (open the UI Labs plugin). `controls` become fields you can change live.
9
+ */
10
+ export = CreateReactStory(
11
+ { react: React, reactRoblox: ReactRoblox, controls: {{ controls }} },
12
+ (props) => <{{ name }} {{ binding }} />,
13
+ );
@@ -0,0 +1,9 @@
1
+ # Build output and generated files: never formatted by hand.
2
+ out
3
+ include
4
+ node_modules
5
+ sourcemap.json
6
+ flamework.build
7
+ package-lock.json
8
+ *.rbxl
9
+ *.rbxlx
@@ -0,0 +1,4 @@
1
+ {
2
+ "useTabs": true,
3
+ "printWidth": 100
4
+ }
@@ -0,0 +1,69 @@
1
+ import { OnStart, Service } from "@flamework/core";
2
+ import type { PlayerData } from "{{ dataImport }}";
3
+ import { PlayerDataService } from "./PlayerDataService";
4
+
5
+ const Players = game.GetService("Players");
6
+
7
+ /**
8
+ * The PlayerData fields shown in the in-game leaderboard, in this order.
9
+ * Add a field name here and it appears for every player. Roblox displays
10
+ * numbers, text and yes/no values.
11
+ */
12
+ const SHOWN: ReadonlyArray<keyof PlayerData> = [{{ shown }}];
13
+
14
+ /**
15
+ * Mirrors player data into Roblox's leaderboard.
16
+ *
17
+ * Roblox shows any Folder named "leaderstats" under a Player, with one value
18
+ * object per column. This service builds it when a player's data has loaded and
19
+ * keeps it in sync when the data changes through PlayerDataService.update.
20
+ * The leaderboard is only a display: never read it back as the source of truth.
21
+ */
22
+ @Service()
23
+ export class LeaderstatsService implements OnStart {
24
+ constructor(private readonly playerData: PlayerDataService) {}
25
+
26
+ onStart(): void {
27
+ this.playerData.onLoaded((player, data) => this.create(player, data));
28
+ this.playerData.onChanged((player, data) => this.refresh(player, data));
29
+
30
+ // Players whose data finished loading before this service started.
31
+ for (const player of Players.GetPlayers()) {
32
+ const data = this.playerData.get(player);
33
+ if (data !== undefined) this.create(player, data);
34
+ }
35
+ }
36
+
37
+ private create(player: Player, data: Readonly<PlayerData>): void {
38
+ if (player.FindFirstChild("leaderstats") !== undefined) return;
39
+
40
+ const folder = new Instance("Folder");
41
+ folder.Name = "leaderstats";
42
+
43
+ for (const field of SHOWN) {
44
+ const value = data[field];
45
+ let stat: NumberValue | StringValue | BoolValue;
46
+
47
+ if (typeIs(value, "number")) stat = new Instance("NumberValue");
48
+ else if (typeIs(value, "boolean")) stat = new Instance("BoolValue");
49
+ else stat = new Instance("StringValue");
50
+
51
+ stat.Name = field;
52
+ stat.Value = value as never;
53
+ stat.Parent = folder;
54
+ }
55
+
56
+ folder.Parent = player;
57
+ }
58
+
59
+ private refresh(player: Player, data: Readonly<PlayerData>): void {
60
+ const folder = player.FindFirstChild("leaderstats");
61
+ if (folder === undefined) return;
62
+
63
+ for (const field of SHOWN) {
64
+ const stat = folder.FindFirstChild(field);
65
+ if (stat === undefined || !stat.IsA("ValueBase")) continue;
66
+ (stat as unknown as { Value: unknown }).Value = data[field];
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,17 @@
1
+ import roblox from "eslint-plugin-roblox-ts";
2
+
3
+ /**
4
+ * ESLint with the official roblox-ts rules.
5
+ *
6
+ * roblox-ts turns TypeScript into Luau, and Luau cannot do everything
7
+ * TypeScript can. These rules catch what the compiler would reject, or worse,
8
+ * accept and get wrong at runtime: `any`, `null` (Luau has `nil`, use
9
+ * `undefined`), unsupported operators, and so on. Run it with `npm run lint`.
10
+ *
11
+ * To turn a rule off or change its level, add an entry after the recommended set:
12
+ * { rules: { "roblox-ts/no-any": "warn" } }
13
+ */
14
+ export default [
15
+ { ignores: ["out/**", "include/**", "node_modules/**"] },
16
+ roblox.configs.recommended,
17
+ ];
@@ -0,0 +1,9 @@
1
+ import { GlobalEvents } from "{{ networkingImport }}";
2
+
3
+ /**
4
+ * The client's side of every event.
5
+ *
6
+ * Events.buyItem.fire(itemId, amount); // send to the server
7
+ * Events.itemBought.connect((itemId) => { ... }); // the server sent one
8
+ */
9
+ export const Events = GlobalEvents.createClient({});
@@ -0,0 +1,22 @@
1
+ import { Networking } from "@flamework/networking";
2
+
3
+ /**
4
+ * Every message between client and server, described once, with types.
5
+ * Roblox RemoteEvents are created and wired for you: nothing to set up by hand.
6
+ *
7
+ * To add an event, add a line in the right interface below:
8
+ *
9
+ * buyItem(itemId: string, amount: number): void;
10
+ *
11
+ * Events the client sends TO the server:
12
+ */
13
+ interface ClientToServerEvents {
14
+ {{ toServer }}
15
+ }
16
+
17
+ /** Events the server sends TO the client(s): */
18
+ interface ServerToClientEvents {
19
+ {{ toClient }}
20
+ }
21
+
22
+ export const GlobalEvents = Networking.createEvent<ClientToServerEvents, ServerToClientEvents>();
@@ -0,0 +1,52 @@
1
+ import type { Networking } from "@flamework/networking";
2
+
3
+ const Players = game.GetService("Players");
4
+
5
+ /**
6
+ * How many times ONE player may send the SAME event per second before the extra
7
+ * ones are ignored. Generous on purpose: normal play never gets near it, a script
8
+ * hammering the server does. Lower it for events that should be rare.
9
+ */
10
+ export const DEFAULT_PER_SECOND = 30;
11
+
12
+ /**
13
+ * Anti-spam for an event the client sends.
14
+ *
15
+ * A client is not trusted: a cheater can fire any event as fast as their computer
16
+ * allows, and every call runs your server code. Without a limit, one player can
17
+ * freeze the server for everyone. Add it to an event in `network.ts`:
18
+ *
19
+ * buyItem: [limit()], // the default
20
+ * openChest: [limit(2)], // at most twice a second
21
+ *
22
+ * Extra messages are dropped, and a warning tells you once per second so the cause
23
+ * is visible in your logs.
24
+ */
25
+ export function limit<I extends readonly unknown[]>(
26
+ perSecond = DEFAULT_PER_SECOND,
27
+ ): Networking.EventMiddleware<I> {
28
+ return (processNext, event) => {
29
+ const windows = new Map<Player, { start: number; count: number }>();
30
+ Players.PlayerRemoving.Connect((player) => windows.delete(player));
31
+
32
+ return (player, ...args) => {
33
+ // Only the server receives from clients, so a player is always there.
34
+ if (player === undefined) return processNext(player, ...args);
35
+
36
+ const now = os.clock();
37
+ const window = windows.get(player);
38
+ if (window === undefined || now - window.start >= 1) {
39
+ windows.set(player, { start: now, count: 1 });
40
+ return processNext(player, ...args);
41
+ }
42
+
43
+ window.count += 1;
44
+ if (window.count <= perSecond) return processNext(player, ...args);
45
+
46
+ // Warn once per window, not once per dropped message: that would be spam too.
47
+ if (window.count === perSecond + 1) {
48
+ warn(`[network] ${player.Name} sends "${event.name}" too fast: extra messages are ignored`);
49
+ }
50
+ };
51
+ };
52
+ }
@@ -0,0 +1,20 @@
1
+ import { GlobalEvents } from "{{ networkingImport }}";
2
+ import { limit } from "{{ rateLimitImport }}";
3
+
4
+ /**
5
+ * The server's side of every event.
6
+ *
7
+ * Events.buyItem.connect((player, itemId, amount) => { ... }); // the client sent one
8
+ * Events.itemBought.fire(player, itemId); // send to one player
9
+ * Events.itemBought.broadcast(itemId); // send to everyone
10
+ *
11
+ * The player who sent an event is always the first argument: clients cannot lie
12
+ * about who they are, so never take a player from the arguments.
13
+ */
14
+ export const Events = GlobalEvents.createServer({
15
+ middleware: {
16
+ // Every event the client sends is limited per player, so one player cannot flood
17
+ // the server. Rowork adds a line here for each new event. Pass a number to change
18
+ // one: limit(2) allows two a second. See rateLimit.ts.{{ entries }}
19
+ },
20
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Everything that is saved for one player.
3
+ *
4
+ * To save something new, add it in TWO places in this file: the interface below
5
+ * and DEFAULT_PLAYER_DATA. TypeScript refuses to compile if you forget one.
6
+ * Players who already have a save get the default the next time they join.
7
+ */
8
+ export interface PlayerData {
9
+ {{ interfaceFields }}
10
+ }
11
+
12
+ /** What a brand-new player starts with. */
13
+ export const DEFAULT_PLAYER_DATA: PlayerData = {
14
+ {{ defaultFields }}
15
+ };