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.
- package/CHANGELOG.md +29 -1
- package/README.md +10 -13
- package/docs/README.md +13 -10
- package/docs/architecture.md +30 -73
- package/docs/cli.md +5 -6
- package/docs/comparison.md +3 -3
- package/docs/config.md +6 -6
- package/docs/cpp-advanced.md +10 -6
- package/docs/cpp-organization.md +4 -2
- package/docs/cpp-safety.md +11 -7
- package/docs/cpp-types.md +1 -1
- package/docs/examples/_category_.json +5 -0
- package/docs/examples/combat.md +239 -0
- package/docs/examples/data-boot.md +98 -0
- package/docs/examples/hud.md +96 -0
- package/docs/examples/index.md +43 -0
- package/docs/examples/leaderstats.md +140 -0
- package/docs/examples/shop.md +122 -0
- package/docs/examples/sword.md +108 -0
- package/docs/getting-started.md +14 -7
- package/docs/intellisense.md +31 -0
- package/docs/intro.md +3 -3
- package/docs/libraries/_category_.json +5 -0
- package/docs/libraries/dataservice.md +104 -0
- package/docs/libraries/index.md +22 -0
- package/docs/libraries/janitor.md +65 -0
- package/docs/libraries/more.md +79 -0
- package/docs/libraries/net.md +35 -0
- package/docs/libraries/promise.md +27 -0
- package/docs/oop/_category_.json +5 -0
- package/docs/oop/file-tags.md +48 -0
- package/docs/oop/index.md +22 -0
- package/docs/oop/modules.md +88 -0
- package/docs/oop/services.md +76 -0
- package/docs/print-cout.md +91 -0
- package/docs/syntax.md +63 -8
- package/editors/vscode/extension.js +146 -0
- package/editors/vscode/package.json +25 -0
- package/include/cluaupp/libs/janitor.hpp +7 -3
- package/include/cluaupp/roblox.hpp +19 -0
- package/package.json +66 -65
- package/runtime/Janitor/init.luau +4 -34
- package/src/architecture.js +174 -66
- package/src/cli.js +88 -21
- package/src/client/init.client.cpp +5 -0
- package/src/compile.js +77 -7
- package/src/editor-install.js +218 -0
- package/src/emit.js +321 -9
- package/src/headers.js +234 -0
- package/src/intellisense.js +1368 -0
- package/src/layout.js +129 -0
- package/src/lex.js +17 -9
- package/src/libs.js +165 -18
- package/src/lsp.js +227 -0
- package/src/parse.js +187 -6
- package/src/preprocess.js +118 -3
- package/src/server/leaderstats.server.cpp +28 -0
- package/src/shared/config.cpp +5 -0
- package/src/shared/config.h +3 -0
- package/src/understand.js +66 -6
- package/templates/game/.clangd +11 -0
- package/templates/game/.vscode/c_cpp_properties.json +6 -2
- package/templates/game/.vscode/extensions.json +6 -0
- package/templates/game/.vscode/settings.json +16 -2
- package/templates/game/cluaupp.config.json +2 -2
- package/templates/game/compile_flags.txt +2 -0
- package/templates/game/src/client/init.client.cpp +1 -0
- package/templates/game/src/server/leaderstats.server.cpp +1 -0
- 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).
|