cluaupp 0.2.2 → 1.0.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 (129) hide show
  1. package/CHANGELOG.md +14 -75
  2. package/README.md +42 -88
  3. package/docs/README.md +25 -21
  4. package/docs/architecture.md +13 -76
  5. package/docs/cli.md +12 -64
  6. package/docs/comparison.md +6 -24
  7. package/docs/config.md +6 -7
  8. package/docs/cpp-advanced.md +6 -3
  9. package/docs/cpp-types.md +2 -0
  10. package/docs/examples/combat.md +19 -227
  11. package/docs/examples/hud.md +13 -81
  12. package/docs/examples/index.md +9 -16
  13. package/docs/examples/leaderstats.md +44 -115
  14. package/docs/getting-started.md +20 -135
  15. package/docs/intellisense.md +5 -42
  16. package/docs/intro.md +16 -15
  17. package/docs/libraries/dataservice.md +4 -1
  18. package/docs/libraries/index.md +2 -1
  19. package/docs/libraries/more.md +3 -1
  20. package/docs/libraries/ui.md +76 -0
  21. package/docs/migration.md +38 -0
  22. package/docs/oop/file-tags.md +12 -36
  23. package/docs/oop/index.md +18 -11
  24. package/docs/roblox-api.md +4 -56
  25. package/docs/syntax.md +54 -179
  26. package/editors/vscode/extension.js +1 -1
  27. package/editors/vscode/package.json +4 -4
  28. package/examples/README.md +5 -7
  29. package/examples/game/.vscode/extensions.json +2 -2
  30. package/examples/game/.vscode/settings.json +6 -23
  31. package/examples/game/src/client/hud.client.clpp +54 -0
  32. package/examples/game/src/client/init.client.clpp +6 -0
  33. package/examples/game/src/server/combat.server.clpp +85 -0
  34. package/examples/game/src/server/leaderstats.server.clpp +28 -0
  35. package/examples/game/src/shared/PlayerData.clh +20 -0
  36. package/examples/game/src/shared/config.clp +6 -0
  37. package/examples/game/src/shared/features.clp +88 -0
  38. package/generated/api.js +15 -1
  39. package/generated/cli.js +40 -24
  40. package/generated/clpp/contract.js +5 -0
  41. package/generated/clpp/index.js +27 -0
  42. package/generated/clpp/paths.js +121 -0
  43. package/generated/clpp/postprocess.js +78 -0
  44. package/generated/clpp/runner.js +102 -0
  45. package/generated/compile.js +26 -138
  46. package/generated/emit.js +79 -3
  47. package/generated/intellisense.js +83 -207
  48. package/generated/lex.js +3 -0
  49. package/generated/libs.js +1 -1
  50. package/generated/lsp.js +1 -8
  51. package/generated/parse.js +119 -24
  52. package/generated/preprocess.js +8 -179
  53. package/generated/system-understander.js +1 -1
  54. package/generated/transpile.js +3 -50
  55. package/generated/utils/project.js +13 -28
  56. package/include/{cluaupp/datatypes.hpp → clpp/datatypes.clh} +5 -1
  57. package/include/{cluaupp/generated/instances.hpp → clpp/generated/instances.clh} +1 -1
  58. package/include/{cluaupp/libs/chrono.hpp → clpp/libs/chrono.clh} +9 -9
  59. package/include/{cluaupp/libs/cmdr.hpp → clpp/libs/cmdr.clh} +4 -4
  60. package/include/{cluaupp/libs/dataservice.hpp → clpp/libs/dataservice.clh} +8 -15
  61. package/include/{cluaupp/libs/display.hpp → clpp/libs/display.clh} +1 -2
  62. package/include/{cluaupp/libs/ezvisual.hpp → clpp/libs/ezvisual.clh} +1 -3
  63. package/include/{cluaupp/libs/formatnumber.hpp → clpp/libs/formatnumber.clh} +2 -3
  64. package/include/{cluaupp/libs/fusion.hpp → clpp/libs/fusion.clh} +5 -5
  65. package/include/{cluaupp/libs/iris.hpp → clpp/libs/iris.clh} +2 -2
  66. package/include/{cluaupp/libs/janitor.hpp → clpp/libs/janitor.clh} +1 -2
  67. package/include/{cluaupp/libs/module3d.hpp → clpp/libs/module3d.clh} +2 -4
  68. package/include/{cluaupp/libs/net.hpp → clpp/libs/net.clh} +2 -4
  69. package/include/{cluaupp/libs/promise.hpp → clpp/libs/promise.clh} +1 -4
  70. package/include/{cluaupp/libs/spring.hpp → clpp/libs/spring.clh} +1 -2
  71. package/include/{cluaupp/libs/statemachine.hpp → clpp/libs/statemachine.clh} +2 -3
  72. package/include/{cluaupp/libs/stickybillboard.hpp → clpp/libs/stickybillboard.clh} +1 -2
  73. package/include/{cluaupp/libs/topbarplus.hpp → clpp/libs/topbarplus.clh} +3 -4
  74. package/include/clpp/libs.clh +24 -0
  75. package/include/clpp/roblox.clh +31 -0
  76. package/package.json +20 -16
  77. package/src/api.ts +15 -1
  78. package/src/cli.ts +39 -25
  79. package/src/clpp/contract.ts +39 -0
  80. package/src/clpp/index.ts +18 -0
  81. package/src/clpp/paths.ts +117 -0
  82. package/src/clpp/postprocess.ts +93 -0
  83. package/src/clpp/runner.ts +102 -0
  84. package/src/compile.ts +26 -149
  85. package/src/intellisense.ts +82 -211
  86. package/src/libs.ts +1 -1
  87. package/src/lsp.ts +1 -5
  88. package/src/preprocess.ts +8 -180
  89. package/src/system-understander.ts +1 -1
  90. package/src/transpile.ts +9 -52
  91. package/src/utils/project.ts +12 -28
  92. package/templates/game/.vscode/extensions.json +2 -2
  93. package/templates/game/.vscode/settings.json +6 -23
  94. package/templates/game/src/client/init.client.clpp +6 -0
  95. package/templates/game/src/server/leaderstats.server.clpp +29 -0
  96. package/templates/game/src/shared/config.clp +6 -0
  97. package/examples/game/.clangd +0 -25
  98. package/examples/game/.vscode/c_cpp_properties.json +0 -27
  99. package/examples/game/compile_flags.txt +0 -6
  100. package/examples/game/src/client/hud.client.cpp +0 -12
  101. package/examples/game/src/client/init.client.cpp +0 -6
  102. package/examples/game/src/server/combat.server.cpp +0 -23
  103. package/examples/game/src/server/leaderstats.server.cpp +0 -29
  104. package/examples/game/src/shared/config.cpp +0 -5
  105. package/examples/game/src/shared/config.h +0 -5
  106. package/include/cluaupp/libs.hpp +0 -25
  107. package/include/cluaupp/roblox.hpp +0 -47
  108. package/src/architecture.ts +0 -500
  109. package/src/ast.ts +0 -37
  110. package/src/emit.ts +0 -531
  111. package/src/emitter/luau-codegen.ts +0 -187
  112. package/src/emitter/translators.ts +0 -168
  113. package/src/headers.ts +0 -233
  114. package/src/layout.ts +0 -114
  115. package/src/lex.ts +0 -155
  116. package/src/parse.ts +0 -630
  117. package/src/parser/collector.ts +0 -148
  118. package/src/parser/index.ts +0 -27
  119. package/src/tree-sitter-cpp.d.ts +0 -4
  120. package/src/understand.ts +0 -322
  121. package/templates/game/.vscode/c_cpp_properties.json +0 -27
  122. package/templates/game/src/client/init.client.cpp +0 -6
  123. package/templates/game/src/server/leaderstats.server.cpp +0 -29
  124. package/templates/game/src/shared/config.cpp +0 -5
  125. package/templates/game/src/shared/config.h +0 -5
  126. /package/include/{cluaupp/generated/enums.hpp → clpp/generated/enums.clh} +0 -0
  127. /package/include/{cluaupp/libs/math.hpp → clpp/libs/math.clh} +0 -0
  128. /package/include/{cluaupp/libs/twinkle.hpp → clpp/libs/twinkle.clh} +0 -0
  129. /package/include/{cluaupp/libs/vfx.hpp → clpp/libs/vfx.clh} +0 -0
@@ -3,237 +3,29 @@ title: Combat (server validation)
3
3
  sidebar_position: 4
4
4
  ---
5
5
 
6
- # Combat (server validation)
6
+ # Combat
7
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.
8
+ Live source: [`examples/game/src/server/combat.server.clpp`](../../examples/game/src/server/combat.server.clpp). Language: [CL++](https://kartzrbx.github.io/CLPP/).
9
9
 
10
- Pair with [Data boot](data-boot.md) if a kill should grant Money.
10
+ This sample uses exclusive CL++:
11
11
 
12
- ## Shared config
12
+ - `guard` / `match` instead of nested null checks
13
+ - `signal<Player*, int> OnHit` and `OnHit~>Connect` / `OnHit::Fire`
14
+ - `observable int combo`
15
+ - `[[server]]` on `WatchWorkspace`
16
+ - `spawn { task::wait(1); … }`
13
17
 
14
- `src/shared/constants/CombatConfig.hpp`
18
+ The client never decides damage. Touched parts fire `OnHit`; the server prints combo and destroys the part.
15
19
 
16
- ```cpp
17
- #pragma once
20
+ ```clpp
21
+ #include <clpp/roblox.clh>
22
+ #include <clpp/libs/janitor.clh>
18
23
 
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;
24
+ struct CombatServer {
25
+ Janitor* janitor;
26
+ signal<Player*, int> OnHit;
27
+ void PlayerEntered(Player* player);
28
+ void BindPart(BasePart* part);
29
+ void WatchWorkspace();
30
+ };
23
31
  ```
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).
@@ -5,92 +5,24 @@ sidebar_position: 6
5
5
 
6
6
  # HUD
7
7
 
8
- Client-only: wait for the replicated profile, write labels, listen for currency changes. No `Set` on persisted paths from the client.
8
+ Live source: [`examples/game/src/client/hud.client.clpp`](../../examples/game/src/client/hud.client.clpp). Fusion API: [CL++](https://kartzrbx.github.io/CLPP/).
9
9
 
10
- Put a ScreenGui named `Hud` in StarterGui with `MoneyLabel` and `LevelLabel` (`TextLabel`).
10
+ Client-only (`[[client]]`). `Fusion:scoped` / `Fusion:Value` / `Fusion:Computed` / `Fusion:New` build a coins label. `guard` fails if `LocalPlayer` is missing.
11
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
- }
12
+ ```clpp
13
+ #include <clpp/roblox.clh>
14
+ #include <clpp/libs/fusion.clh>
61
15
 
16
+ [[client]]
62
17
  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;
18
+ Players* players = GetService<Players>();
19
+ Player* localPlayer = players.LocalPlayer;
20
+ guard (localPlayer != null) else {
21
+ report("LocalPlayer missing");
75
22
  return;
76
23
  }
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));
24
+ auto scope = Fusion:scoped();
25
+ auto coins = Fusion:Value(scope, 0);
26
+ coins(100);
87
27
  }
88
28
  ```
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`).
@@ -5,28 +5,21 @@ sidebar_position: 1
5
5
 
6
6
  # Examples
7
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.
8
+ High-quality Cluaupp systems you can copy. Each page is a full service in **CL++**. Live sample: [`examples/game`](../../examples/game). Language reference: [kartzrbx.github.io/CLPP](https://kartzrbx.github.io/CLPP/).
9
9
 
10
- In this repo the live sample is [`examples/game`](../../examples/game) (`src/server`, `src/client`, `src/shared`). Compiler TypeScript is never mixed with game C++. `cluaupp init` still copies [`templates/game`](../../templates/game).
11
-
12
- These are **not** dumps of `int main()`. Entry is `void init()`. There are no lambdas and no custom C++ classes — named functions + structs.
10
+ These are **not** dumps of `int main()`. Entry is `void init()`. Types are `struct` + `Class::` methods. Canonical Leaderstats: [handbook](https://kartzrbx.github.io/Cluaupp/docs/leaderstats.html).
13
11
 
14
12
  ## Suggested layout
15
13
 
16
14
  ```
17
15
  src/
18
- shared/constants/TemplateData.hpp
19
- shared/constants/CombatConfig.hpp
20
- shared/constants/ShopCatalog.hpp
21
- server/boot/DataBoot.server.cpp
22
- client/boot/DataBoot.client.cpp
23
- server/services/leaderstats/LeaderstatsServer.server.cpp
24
- server/services/combat/CombatServer.server.cpp
25
- client/controllers/combat/CombatClient.client.cpp
26
- server/services/shop/ShopServer.server.cpp
27
- client/controllers/shop/ShopClient.client.cpp
28
- client/controllers/hud/HudClient.client.cpp
29
- server/services/weapons/SwordServer.server.cpp
16
+ shared/PlayerData.clh
17
+ shared/config.clp
18
+ shared/features.clp
19
+ server/leaderstats.server.clpp
20
+ server/combat.server.clpp
21
+ client/init.client.clpp
22
+ client/hud.client.clpp
30
23
  ```
31
24
 
32
25
  Boot DataService **once**. Other services `WaitFor` after that.
@@ -5,136 +5,65 @@ sidebar_position: 3
5
5
 
6
6
  # Leaderstats
7
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.
8
+ Canonical copy (three files): [Docs → Example: Leaderstats](https://kartzrbx.github.io/Cluaupp/docs/leaderstats.html).
9
9
 
10
- It does **not** call `DataService.Init`. Boot that in [Data boot](data-boot.md).
10
+ Roblox shows the player list from a Folder named exactly `leaderstats` under the Player. This service **creates** those values, then mirrors DataService coins into them.
11
11
 
12
- ## Why this shape
12
+ It does **not** call `DataService.Init`. Boot that in [Data boot](data-boot.md) with your `PlayerData` Template.
13
+
14
+ ## Files
15
+
16
+ | File | Role |
17
+ | --- | --- |
18
+ | `shared/PlayerData.h` | Template structs (`Currencies.Coins`, inventory `LuaArray`) |
19
+ | `server/LeaderstatsServer.h` | `struct LeaderstatsServer` — fields + method decls (same stem) |
20
+ | `server/LeaderstatsServer.server.cpp` | `Class::` bodies + `void init()` |
21
+
22
+ ## Rules
13
23
 
14
24
  | Rule | Why |
15
25
  | --- | --- |
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) |
26
+ | Same-stem header | `LeaderstatsServer.h` + `LeaderstatsServer.server.cpp`. A differently named `leaderstats.h` is a `require`. |
27
+ | `.server.cpp` | Untagged cpp is a ModuleScript; `init()` will not run by itself. |
28
+ | `Paths.Currencies.Coins` | After `Init`. A local `int Coins` is not a Data path. |
29
+ | `string_concat` or string `+` | Luau `..`. Do not write `..` in the `.cpp`. |
30
+ | Janitor keyed by player name | Leaving the game must `Destroy` the folder. |
31
+ | Lambda or named function | `GetChangedSignal(...).Connect([coinsValue](int n) { ... })` is valid. |
22
32
 
23
- ## `LeaderstatsServer.server.cpp`
33
+ ## Header
24
34
 
25
35
  ```cpp
36
+ #pragma once
26
37
  #include <cluaupp/roblox.hpp>
27
38
  #include <cluaupp/libs/janitor.hpp>
28
39
  #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
40
 
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
- }
41
+ struct LeaderstatsServer {
42
+ static constexpr int STARTING_COINS = 0;
43
+ Janitor* janitor;
44
+ string GetPlayerJanitorKey(Player* player);
45
+ void UpdateLeaderstatsWithValues(IntValue* currentValue, int newValue);
46
+ Folder* EnsurePlayerLeaderstatsFolder(Player* player);
47
+ void PlayerEntered(Player* player);
48
+ };
49
+ ```
101
50
 
102
- void OnPlayerRemoving(Player* player) {
103
- if (janitor->Get(player->Name)) {
104
- janitor->Remove(player->Name);
105
- }
106
- }
51
+ ## `PlayerEntered`
107
52
 
108
- void OnClose() {
109
- janitor->Destroy();
110
- }
111
-
112
- void init() {
113
- for (Player* player : Players->GetPlayers()) {
114
- SetupPlayer(player);
53
+ ```cpp
54
+ void LeaderstatsServer::PlayerEntered(Player* player) {
55
+ Folder* leaderstatsFolder = EnsurePlayerLeaderstatsFolder(player);
56
+ Data* playerData = DataService::Server.WaitFor(player);
57
+ IntValue* coinsValue = static_cast<IntValue*>(leaderstatsFolder->FindFirstChild("Coins"));
58
+ if (coinsValue) {
59
+ playerData->GetChangedSignal(DataService::Server.Paths.Currencies.Coins).Connect(
60
+ [coinsValue](int newValue) {
61
+ UpdateLeaderstatsWithValues(coinsValue, newValue);
62
+ }
63
+ );
115
64
  }
116
- janitor->Add(Players->PlayerAdded.Connect(SetupPlayer));
117
- janitor->Add(Players->PlayerRemoving.Connect(OnPlayerRemoving));
118
- game->BindToClose(OnClose);
65
+ janitor->Add(leaderstatsFolder, "Destroy", GetPlayerJanitorKey(player));
119
66
  }
120
67
  ```
121
68
 
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).
69
+ `init()` constructs `LeaderstatsServer`, assigns `janitor = new Janitor()`, runs `PlayerEntered` for everyone already in the game, then `PlayerAdded` + `BindToClose`. Full listing is on the [handbook page](https://kartzrbx.github.io/Cluaupp/docs/leaderstats.html).