cluaupp 0.1.2 → 0.1.5

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 (69) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +10 -13
  3. package/docs/README.md +13 -10
  4. package/docs/architecture.md +30 -73
  5. package/docs/cli.md +5 -6
  6. package/docs/comparison.md +3 -3
  7. package/docs/config.md +6 -6
  8. package/docs/cpp-advanced.md +10 -6
  9. package/docs/cpp-organization.md +4 -2
  10. package/docs/cpp-safety.md +11 -7
  11. package/docs/cpp-types.md +1 -1
  12. package/docs/examples/_category_.json +5 -0
  13. package/docs/examples/combat.md +239 -0
  14. package/docs/examples/data-boot.md +98 -0
  15. package/docs/examples/hud.md +96 -0
  16. package/docs/examples/index.md +43 -0
  17. package/docs/examples/leaderstats.md +140 -0
  18. package/docs/examples/shop.md +122 -0
  19. package/docs/examples/sword.md +108 -0
  20. package/docs/getting-started.md +14 -7
  21. package/docs/intellisense.md +31 -0
  22. package/docs/intro.md +3 -3
  23. package/docs/libraries/_category_.json +5 -0
  24. package/docs/libraries/dataservice.md +104 -0
  25. package/docs/libraries/index.md +22 -0
  26. package/docs/libraries/janitor.md +65 -0
  27. package/docs/libraries/more.md +79 -0
  28. package/docs/libraries/net.md +35 -0
  29. package/docs/libraries/promise.md +27 -0
  30. package/docs/oop/_category_.json +5 -0
  31. package/docs/oop/file-tags.md +48 -0
  32. package/docs/oop/index.md +22 -0
  33. package/docs/oop/modules.md +88 -0
  34. package/docs/oop/services.md +76 -0
  35. package/docs/print-cout.md +91 -0
  36. package/docs/syntax.md +63 -8
  37. package/editors/vscode/extension.js +146 -0
  38. package/editors/vscode/package.json +25 -0
  39. package/include/cluaupp/libs/janitor.hpp +7 -3
  40. package/include/cluaupp/roblox.hpp +19 -0
  41. package/package.json +66 -65
  42. package/runtime/Janitor/init.luau +4 -34
  43. package/src/architecture.js +174 -66
  44. package/src/cli.js +88 -21
  45. package/src/client/init.client.cpp +5 -0
  46. package/src/compile.js +77 -7
  47. package/src/editor-install.js +218 -0
  48. package/src/emit.js +321 -9
  49. package/src/headers.js +234 -0
  50. package/src/intellisense.js +1368 -0
  51. package/src/layout.js +129 -0
  52. package/src/lex.js +17 -9
  53. package/src/libs.js +165 -18
  54. package/src/lsp.js +227 -0
  55. package/src/parse.js +187 -6
  56. package/src/preprocess.js +118 -3
  57. package/src/server/leaderstats.server.cpp +28 -0
  58. package/src/shared/config.cpp +5 -0
  59. package/src/shared/config.h +3 -0
  60. package/src/understand.js +66 -6
  61. package/templates/game/.clangd +11 -0
  62. package/templates/game/.vscode/c_cpp_properties.json +6 -2
  63. package/templates/game/.vscode/extensions.json +6 -0
  64. package/templates/game/.vscode/settings.json +16 -2
  65. package/templates/game/cluaupp.config.json +2 -2
  66. package/templates/game/compile_flags.txt +2 -0
  67. package/templates/game/src/client/init.client.cpp +1 -0
  68. package/templates/game/src/server/leaderstats.server.cpp +1 -0
  69. package/docs/libraries.md +0 -80
@@ -0,0 +1,239 @@
1
+ ---
2
+ title: Combat (server validation)
3
+ sidebar_position: 4
4
+ ---
5
+
6
+ # Combat (server validation)
7
+
8
+ The client never sends **damage**. It sends **who I want to hit**. The server decides range, cooldown, alive state, and the damage constant.
9
+
10
+ Pair with [Data boot](data-boot.md) if a kill should grant Money.
11
+
12
+ ## Shared config
13
+
14
+ `src/shared/constants/CombatConfig.hpp`
15
+
16
+ ```cpp
17
+ #pragma once
18
+
19
+ const double ATTACK_RANGE = 12;
20
+ const double ATTACK_COOLDOWN = 0.4;
21
+ const int ATTACK_DAMAGE = 12;
22
+ const int KILL_REWARD = 5;
23
+ ```
24
+
25
+ Keep numbers in a header. Both server and client can include range for FX; **only the server** applies `TakeDamage`.
26
+
27
+ ## Server — `CombatServer.server.cpp`
28
+
29
+ ```cpp
30
+ #include <cluaupp/roblox.hpp>
31
+ #include <cluaupp/libs/janitor.hpp>
32
+ #include <cluaupp/libs/net.hpp>
33
+ #include <cluaupp/libs/dataservice.hpp>
34
+ #include "../../../shared/constants/CombatConfig.hpp"
35
+
36
+ Players* Players = GetService<Players>();
37
+ Janitor* janitor = new Janitor();
38
+ NetEvent* Attack = Net::Event("Attack");
39
+
40
+ BasePart* RootPart(Model* character) {
41
+ if (character == nullptr) {
42
+ return nullptr;
43
+ }
44
+ return character->FindFirstChild("HumanoidRootPart");
45
+ }
46
+
47
+ Humanoid* HumanoidOf(Model* character) {
48
+ if (character == nullptr) {
49
+ return nullptr;
50
+ }
51
+ return character->FindFirstChildOfClass("Humanoid");
52
+ }
53
+
54
+ bool InRange(Model* a, Model* b, double maxRange) {
55
+ BasePart* rootA = RootPart(a);
56
+ BasePart* rootB = RootPart(b);
57
+ if (rootA == nullptr) {
58
+ return false;
59
+ }
60
+ if (rootB == nullptr) {
61
+ return false;
62
+ }
63
+ Vector3 delta = rootA->Position - rootB->Position;
64
+ if (delta.Magnitude > maxRange) {
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+
70
+ bool OnCooldown(Player* attacker) {
71
+ NumberValue* last = attacker->FindFirstChild("LastAttackAt");
72
+ if (last == nullptr) {
73
+ return false;
74
+ }
75
+ if (tick() - last->Value < ATTACK_COOLDOWN) {
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ void MarkAttack(Player* attacker) {
82
+ NumberValue* last = attacker->FindFirstChild("LastAttackAt");
83
+ if (last == nullptr) {
84
+ last = new NumberValue(attacker);
85
+ last->Name = "LastAttackAt";
86
+ }
87
+ last->Value = tick();
88
+ }
89
+
90
+ void RewardKill(Player* attacker) {
91
+ Data* data = DataService::Server.Get(attacker);
92
+ if (data == nullptr) {
93
+ return;
94
+ }
95
+ int money = data->Get(DataService::Server.Paths.Currencies.Money);
96
+ data->Set(DataService::Server.Paths.Currencies.Money, money + KILL_REWARD);
97
+ }
98
+
99
+ void OnAttack(Player* attacker, int targetUserId) {
100
+ if (attacker == nullptr) {
101
+ return;
102
+ }
103
+ if (targetUserId < 1) {
104
+ return;
105
+ }
106
+ if (OnCooldown(attacker)) {
107
+ return;
108
+ }
109
+
110
+ Player* target = Players->GetPlayerByUserId(targetUserId);
111
+ if (target == nullptr) {
112
+ return;
113
+ }
114
+ if (target == attacker) {
115
+ return;
116
+ }
117
+
118
+ Model* attackerChar = attacker->Character;
119
+ Model* targetChar = target->Character;
120
+ Humanoid* attackerHum = HumanoidOf(attackerChar);
121
+ Humanoid* targetHum = HumanoidOf(targetChar);
122
+ if (attackerHum == nullptr) {
123
+ return;
124
+ }
125
+ if (targetHum == nullptr) {
126
+ return;
127
+ }
128
+ if (attackerHum->Health <= 0) {
129
+ return;
130
+ }
131
+ if (targetHum->Health <= 0) {
132
+ return;
133
+ }
134
+ if (InRange(attackerChar, targetChar, ATTACK_RANGE) == false) {
135
+ cout::ping << attacker->Name << " out of range" << endl;
136
+ return;
137
+ }
138
+
139
+ MarkAttack(attacker);
140
+ double healthBefore = targetHum->Health;
141
+ targetHum->TakeDamage(ATTACK_DAMAGE);
142
+ if (healthBefore > 0) {
143
+ if (targetHum->Health <= 0) {
144
+ RewardKill(attacker);
145
+ }
146
+ }
147
+ }
148
+
149
+ void init() {
150
+ Attack->On(OnAttack);
151
+ janitor->Add(Attack);
152
+ }
153
+ ```
154
+
155
+ ## Client — `CombatClient.client.cpp`
156
+
157
+ The LocalScript only picks a target and fires. It does **not** pass `ATTACK_DAMAGE`.
158
+
159
+ ```cpp
160
+ #include <cluaupp/roblox.hpp>
161
+ #include <cluaupp/libs/janitor.hpp>
162
+ #include <cluaupp/libs/net.hpp>
163
+
164
+ Players* Players = GetService<Players>();
165
+ UserInputService* UserInput = GetService<UserInputService>();
166
+ Janitor* janitor = new Janitor();
167
+ NetEvent* Attack = Net::Event("Attack");
168
+
169
+ Player* PlayerFromPart(Instance* part) {
170
+ if (part == nullptr) {
171
+ return nullptr;
172
+ }
173
+ Instance* model = part->FindFirstAncestorOfClass("Model");
174
+ if (model == nullptr) {
175
+ return nullptr;
176
+ }
177
+ Player* fromModel = Players->GetPlayerFromCharacter(model);
178
+ if (fromModel != nullptr) {
179
+ return fromModel;
180
+ }
181
+ Instance* outer = model->FindFirstAncestorOfClass("Model");
182
+ if (outer == nullptr) {
183
+ return nullptr;
184
+ }
185
+ return Players->GetPlayerFromCharacter(outer);
186
+ }
187
+
188
+ void OnInputBegan(InputObject* input, bool gameProcessed) {
189
+ if (gameProcessed) {
190
+ return;
191
+ }
192
+ if (input->UserInputType != Enum::UserInputType::MouseButton1) {
193
+ return;
194
+ }
195
+ Player* localPlayer = Players->LocalPlayer;
196
+ if (localPlayer == nullptr) {
197
+ return;
198
+ }
199
+ Mouse* mouse = localPlayer->GetMouse();
200
+ if (mouse == nullptr) {
201
+ return;
202
+ }
203
+ Player* target = PlayerFromPart(mouse->Target);
204
+ if (target == nullptr) {
205
+ return;
206
+ }
207
+ if (target == localPlayer) {
208
+ return;
209
+ }
210
+ Attack->FireServer(target->UserId);
211
+ }
212
+
213
+ void init() {
214
+ janitor->Add(UserInput->InputBegan.Connect(OnInputBegan));
215
+ }
216
+ ```
217
+
218
+ ## What the server rejects
219
+
220
+ | Client cheat | Server check |
221
+ | --- | --- |
222
+ | `FireServer(99999)` damage | Damage is `ATTACK_DAMAGE` in the header — not an argument |
223
+ | Hit a player across the map | `delta.Magnitude > ATTACK_RANGE` |
224
+ | Spam click | `LastAttackAt` + `ATTACK_COOLDOWN` |
225
+ | Target userId `0` / self | `targetUserId < 1`, `target == attacker` |
226
+ | Hit a dead / missing character | Humanoid nil or `Health <= 0` |
227
+ | Fire before spawn | `Character` / `HumanoidRootPart` missing |
228
+
229
+ `Net::Event("Attack")` must be the **same string** on both sides. Treat that name as public protocol.
230
+
231
+ ## Cooldown storage
232
+
233
+ `LastAttackAt` is a `NumberValue` on the Player: session-only, no DataStore. Do not persist combat cadence. For anti-exploit that must survive respawn, use DataService `SetTransient` on a path **you** added to the Template — still not a library field.
234
+
235
+ ## No lambdas
236
+
237
+ `Attack->On(OnAttack)` needs a named function. The first argument of a server `On` is the `Player` who fired.
238
+
239
+ See [Net](../libraries/net.md) and [Safety](../cpp-safety.md).
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: Data boot
3
+ sidebar_position: 2
4
+ ---
5
+
6
+ # Data boot
7
+
8
+ Start DataService **once** on the server and once on the client. Gameplay scripts (`leaderstats`, combat, shop) only `WaitFor` — they do not call `Init`.
9
+
10
+ The save **shape is yours**. Do not put `Currencies` on the library `DataPath` type. Put it on a shared struct and pass that as `.Template`.
11
+
12
+ ## Shared template
13
+
14
+ `src/shared/constants/TemplateData.hpp`
15
+
16
+ ```cpp
17
+ #pragma once
18
+
19
+ struct TemplateData {
20
+ struct Currencies {
21
+ int Money = 0;
22
+ int Level = 1;
23
+ } Currencies;
24
+ };
25
+ ```
26
+
27
+ `src/server/configurations/PlayerDataVersion.hpp`
28
+
29
+ ```cpp
30
+ #pragma once
31
+
32
+ const string PLAYER_DATA_VERSION = "PlayerData_v1";
33
+ ```
34
+
35
+ Bump the store name when you **intentionally** wipe saves. Changing a field default in `TemplateData` does not migrate old profiles by itself.
36
+
37
+ ## Server — `DataBoot.server.cpp`
38
+
39
+ Use `void init()`, not `int main()`. Cluaupp only auto-calls `init()`.
40
+
41
+ ```cpp
42
+ #include <cluaupp/roblox.hpp>
43
+ #include <cluaupp/libs/dataservice.hpp>
44
+ #include "../../shared/constants/TemplateData.hpp"
45
+ #include "../configurations/PlayerDataVersion.hpp"
46
+
47
+ void init() {
48
+ TemplateData playerData = TemplateData {};
49
+ DataService::Server.Init(DataServiceOptions {
50
+ .Template = playerData,
51
+ .StoreName = PLAYER_DATA_VERSION,
52
+ .UseMock = true,
53
+ });
54
+ }
55
+ ```
56
+
57
+ | Field | In production |
58
+ | --- | --- |
59
+ | `.Template` | Same struct the rest of the game reads through `Paths` |
60
+ | `.StoreName` | Stable DataStore name (version it when you wipe) |
61
+ | `.UseMock` | `true` in Studio so you do not hit the live store |
62
+
63
+ After `Init`, `DataService::Server.Paths.Currencies.Money` exists because the Template had that table — not because the library shipped those fields.
64
+
65
+ ## Client — `DataBoot.client.cpp`
66
+
67
+ ```cpp
68
+ #include <cluaupp/roblox.hpp>
69
+ #include <cluaupp/libs/dataservice.hpp>
70
+
71
+ void init() {
72
+ DataService::Client.Init();
73
+ }
74
+ ```
75
+
76
+ Client `Init` has no Template. The server already owns the document.
77
+
78
+ ## What other scripts do
79
+
80
+ ```cpp
81
+ Data* data = DataService::Server.WaitFor(player);
82
+ if (data == nullptr) {
83
+ return;
84
+ }
85
+
86
+ int money = data->Get(DataService::Server.Paths.Currencies.Money);
87
+ ```
88
+
89
+ - Server gameplay: `WaitFor(player)` then `Get` / `Set`.
90
+ - Client HUD: `WaitForData()` then `Get`.
91
+ - Never `Init` twice. A second `Init` fights the first store.
92
+
93
+ ## File tags
94
+
95
+ `DataBoot.server.cpp` → Script **RunContext Server** (`init.luau` + `init.meta.json`).
96
+ `DataBoot.client.cpp` → LocalScript (`init.client.luau`).
97
+
98
+ See [file tags](../oop/file-tags.md).
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: HUD
3
+ sidebar_position: 6
4
+ ---
5
+
6
+ # HUD
7
+
8
+ Client-only: wait for the replicated profile, write labels, listen for currency changes. No `Set` on persisted paths from the client.
9
+
10
+ Put a ScreenGui named `Hud` in StarterGui with `MoneyLabel` and `LevelLabel` (`TextLabel`).
11
+
12
+ Define callbacks **above** `init()` so clangd and the subset both see them (no lambdas).
13
+
14
+ ## `HudClient.client.cpp`
15
+
16
+ ```cpp
17
+ #include <cluaupp/roblox.hpp>
18
+ #include <cluaupp/libs/janitor.hpp>
19
+ #include <cluaupp/libs/dataservice.hpp>
20
+ #include <cluaupp/libs/formatnumber.hpp>
21
+ #include <cluaupp/libs/twinkle.hpp>
22
+
23
+ Players* Players = GetService<Players>();
24
+ Janitor* janitor = new Janitor();
25
+
26
+ TextLabel* FindLabel(PlayerGui* playerGui, string name) {
27
+ if (playerGui == nullptr) {
28
+ return nullptr;
29
+ }
30
+ Instance* gui = playerGui->FindFirstChild("Hud");
31
+ if (gui == nullptr) {
32
+ return nullptr;
33
+ }
34
+ return gui->FindFirstChild(name);
35
+ }
36
+
37
+ void Render(Data* data, TextLabel* moneyLabel, TextLabel* levelLabel) {
38
+ if (data == nullptr) {
39
+ return;
40
+ }
41
+ int money = data->Get(DataService::Client.Paths.Currencies.Money);
42
+ int level = data->Get(DataService::Client.Paths.Currencies.Level);
43
+ if (moneyLabel != nullptr) {
44
+ moneyLabel->Text = FormatNumber::Abbreviate(money);
45
+ }
46
+ if (levelLabel != nullptr) {
47
+ levelLabel->Text = FormatNumber::Comma(level);
48
+ }
49
+ }
50
+
51
+ void OnCurrenciesChanged() {
52
+ Player* player = Players->LocalPlayer;
53
+ if (player == nullptr) {
54
+ return;
55
+ }
56
+ PlayerGui* playerGui = player->FindFirstChildOfClass("PlayerGui");
57
+ TextLabel* moneyLabel = FindLabel(playerGui, "MoneyLabel");
58
+ TextLabel* levelLabel = FindLabel(playerGui, "LevelLabel");
59
+ Render(DataService::Client.Get(), moneyLabel, levelLabel);
60
+ }
61
+
62
+ void init() {
63
+ Player* player = Players->LocalPlayer;
64
+ if (player == nullptr) {
65
+ return;
66
+ }
67
+ PlayerGui* playerGui = player->FindFirstChildOfClass("PlayerGui");
68
+ if (playerGui == nullptr) {
69
+ return;
70
+ }
71
+
72
+ Data* data = DataService::Client.WaitForData();
73
+ if (data == nullptr) {
74
+ cout::warn << "HUD: profile missing" << endl;
75
+ return;
76
+ }
77
+
78
+ TextLabel* moneyLabel = FindLabel(playerGui, "MoneyLabel");
79
+ TextLabel* levelLabel = FindLabel(playerGui, "LevelLabel");
80
+ Render(data, moneyLabel, levelLabel);
81
+
82
+ if (moneyLabel != nullptr) {
83
+ Twinkle::Fade(moneyLabel, true);
84
+ }
85
+
86
+ janitor->Add(data->GetChangedSignal(DataService::Client.Paths.Currencies).Connect(OnCurrenciesChanged));
87
+ }
88
+ ```
89
+
90
+ ## Rules
91
+
92
+ - Call `DataService::Client.Init()` in [Data boot](data-boot.md) first. Script order in StarterPlayerScripts is not a contract — `WaitForData` yields until the profile exists.
93
+ - Do not `data->Set` Money from the HUD. Display only.
94
+ - `Twinkle::Fade` is optional. See [more libraries](../libraries/more.md).
95
+
96
+ `HudClient.client.cpp` is a LocalScript (`init.client.luau`).
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Examples
3
+ sidebar_position: 1
4
+ ---
5
+
6
+ # Examples
7
+
8
+ High-quality Cluaupp systems you can copy. Each page is a full service: C++ that the subset actually compiles, server authority, Janitor cleanup, and the file names Studio expects.
9
+
10
+ These are **not** dumps of `int main()`. Entry is `void init()`. There are no lambdas and no custom C++ classes — named functions + structs.
11
+
12
+ ## Suggested layout
13
+
14
+ ```
15
+ src/
16
+ shared/constants/TemplateData.hpp
17
+ shared/constants/CombatConfig.hpp
18
+ shared/constants/ShopCatalog.hpp
19
+ server/boot/DataBoot.server.cpp
20
+ client/boot/DataBoot.client.cpp
21
+ server/services/leaderstats/LeaderstatsServer.server.cpp
22
+ server/services/combat/CombatServer.server.cpp
23
+ client/controllers/combat/CombatClient.client.cpp
24
+ server/services/shop/ShopServer.server.cpp
25
+ client/controllers/shop/ShopClient.client.cpp
26
+ client/controllers/hud/HudClient.client.cpp
27
+ server/services/weapons/SwordServer.server.cpp
28
+ ```
29
+
30
+ Boot DataService **once**. Other services `WaitFor` after that.
31
+
32
+ ## Catalog
33
+
34
+ | Example | Teaches |
35
+ | --- | --- |
36
+ | [Data boot](data-boot.md) | `Server.Init` / `Client.Init`, your Template, not `main()` |
37
+ | [Leaderstats](leaderstats.md) | Folder `leaderstats`, FormatNumber, `GetChangedSignal`, per-player Janitor |
38
+ | [Combat (server validation)](combat.md) | Client sends **intent**; server checks range, cooldown, health, then `TakeDamage` |
39
+ | [Shop](shop.md) | `Net` buy remote, prices on the server, DataService debit |
40
+ | [HUD](hud.md) | Client `WaitForData`, labels, Twinkle |
41
+ | [Sword / Touched](sword.md) | Hitbox on the **server**, debounce, no client damage |
42
+
43
+ Read with [Libraries](../libraries/index.md), [OOP structure](../oop/index.md), and [Safety](../cpp-safety.md).
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: Leaderstats
3
+ sidebar_position: 3
4
+ ---
5
+
6
+ # Leaderstats
7
+
8
+ Roblox shows the player list from a Folder named exactly `leaderstats` under the Player, with `IntValue` / `StringValue` children. This service **creates** those values, then mirrors DataService currencies into them.
9
+
10
+ It does **not** call `DataService.Init`. Boot that in [Data boot](data-boot.md).
11
+
12
+ ## Why this shape
13
+
14
+ | Rule | Why |
15
+ | --- | --- |
16
+ | Create the Folder if missing | `FindFirstChild` is not a constructor |
17
+ | Create the stat if missing | Returning early when it is absent never shows Money |
18
+ | `StringValue` + `FormatNumber::Abbreviate` | Player list wants a string like `1.5K` |
19
+ | Janitor per player, keyed by `player->Name` | Leaving the game must `Destroy` the section janitor |
20
+ | `GetChangedSignal(Paths.Currencies)` | HUD / list update without polling |
21
+ | Named function, not a lambda | Cluaupp has no lambdas. The shared callback refreshes **all** players (currency writes are rare) |
22
+
23
+ ## `LeaderstatsServer.server.cpp`
24
+
25
+ ```cpp
26
+ #include <cluaupp/roblox.hpp>
27
+ #include <cluaupp/libs/janitor.hpp>
28
+ #include <cluaupp/libs/dataservice.hpp>
29
+ #include <cluaupp/libs/formatnumber.hpp>
30
+
31
+ Players* Players = GetService<Players>();
32
+ Janitor* janitor = new Janitor();
33
+
34
+ void EnsureStat(Folder* leaderstats, string name) {
35
+ Instance* existing = leaderstats->FindFirstChild(name);
36
+ if (existing != nullptr) {
37
+ return;
38
+ }
39
+ StringValue* stat = new StringValue(leaderstats);
40
+ stat->Name = name;
41
+ stat->Value = "0";
42
+ }
43
+
44
+ void SetStat(Folder* leaderstats, string name, int value) {
45
+ StringValue* stat = leaderstats->FindFirstChild(name);
46
+ if (stat == nullptr) {
47
+ cout::warn << "leaderstats missing " << name << endl;
48
+ return;
49
+ }
50
+ stat->Value = FormatNumber::Abbreviate(value);
51
+ }
52
+
53
+ Folder* EnsureLeaderstats(Player* player) {
54
+ Folder* leaderstats = player->FindFirstChild("leaderstats");
55
+ if (leaderstats != nullptr) {
56
+ return leaderstats;
57
+ }
58
+ leaderstats = new Folder(player);
59
+ leaderstats->Name = "leaderstats";
60
+ return leaderstats;
61
+ }
62
+
63
+ void ApplyCurrencies(Player* player) {
64
+ Data* data = DataService::Server.Get(player);
65
+ if (data == nullptr) {
66
+ return;
67
+ }
68
+ Folder* leaderstats = player->FindFirstChild("leaderstats");
69
+ if (leaderstats == nullptr) {
70
+ return;
71
+ }
72
+ int money = data->Get(DataService::Server.Paths.Currencies.Money);
73
+ int level = data->Get(DataService::Server.Paths.Currencies.Level);
74
+ SetStat(leaderstats, "Money", money);
75
+ SetStat(leaderstats, "Level", level);
76
+ }
77
+
78
+ void OnCurrenciesChanged() {
79
+ for (Player* player : Players->GetPlayers()) {
80
+ ApplyCurrencies(player);
81
+ }
82
+ }
83
+
84
+ void SetupPlayer(Player* player) {
85
+ Data* data = DataService::Server.WaitFor(player);
86
+ if (data == nullptr) {
87
+ cout::warn << "no profile for " << player->Name << endl;
88
+ return;
89
+ }
90
+
91
+ Folder* leaderstats = EnsureLeaderstats(player);
92
+ EnsureStat(leaderstats, "Money");
93
+ EnsureStat(leaderstats, "Level");
94
+ ApplyCurrencies(player);
95
+
96
+ Janitor* section = new Janitor();
97
+ section->Add(data->GetChangedSignal(DataService::Server.Paths.Currencies).Connect(OnCurrenciesChanged));
98
+ section->Add(leaderstats, "Destroy");
99
+ janitor->Add(section, "Destroy", player->Name);
100
+ }
101
+
102
+ void OnPlayerRemoving(Player* player) {
103
+ if (janitor->Get(player->Name)) {
104
+ janitor->Remove(player->Name);
105
+ }
106
+ }
107
+
108
+ void OnClose() {
109
+ janitor->Destroy();
110
+ }
111
+
112
+ void init() {
113
+ for (Player* player : Players->GetPlayers()) {
114
+ SetupPlayer(player);
115
+ }
116
+ janitor->Add(Players->PlayerAdded.Connect(SetupPlayer));
117
+ janitor->Add(Players->PlayerRemoving.Connect(OnPlayerRemoving));
118
+ game->BindToClose(OnClose);
119
+ }
120
+ ```
121
+
122
+ ## Output
123
+
124
+ `LeaderstatsServer.server.cpp` becomes a service folder: `init.luau` (Script, RunContext **Server**), `Main`, `PlayersManager`, `DataController` / `CacheController`, Types. Your functions stay in the domain controller. See [services](../oop/services.md).
125
+
126
+ ## Bugs this example avoids
127
+
128
+ | Broken | Correct |
129
+ | --- | --- |
130
+ | `if (!existing) { return; }` then never create the value | `EnsureStat` **creates** when missing |
131
+ | `if (!money) { money->Value = ... }` | That writes only when the child is **nil** (crash / no-op). Set when the child **exists** |
132
+ | `DataService.Get` before `WaitFor` on join | `WaitFor` in `SetupPlayer`, `Get` in the refresh path |
133
+ | `int main()` | `void init()` |
134
+ | One global connection, never removed | Section janitor destroyed on `PlayerRemoving` |
135
+
136
+ `Paths.Currencies` is valid **after** your Template was passed to `Init`. The library header does not define those fields.
137
+
138
+ ## IntValue vs StringValue
139
+
140
+ Use `IntValue` if you want the default numeric sort and no abbreviation. Use `StringValue` + `FormatNumber::Abbreviate` for `1.5K`. Do not mix both names (`Money` twice).