Get started
Project layout
What lives in src/shared, src/server and src/client, and how data gets from the server to the UI.
All the game code is in boilerplate/src/, split by where it runs. Rojo puts shared/ in ReplicatedStorage, server/ in ServerScriptService and client/ in StarterPlayerScripts.
src/shared
Code and config that both sides load. Players can read everything here, so nothing secret goes in it.
| File | What it holds |
|---|---|
Config.ts |
Currencies, products, passes, multipliers, boosts, items, variants, inventory limits, the index, social rewards, daily and playtime rewards, onboarding steps, leaderboard, admins, audio, DataStore names, trading |
EggsConfig.ts |
Eggs: contents, weights, prices, pity, Robux products |
WheelConfig.ts |
Spin wheel prizes, weights, free spin timer, spin products |
QuestsConfig.ts |
Quest boards, achievements, the Goal type and goalText |
SeasonConfig.ts |
Season pass settings and the list of seasons |
Remotes.ts |
Every remote in the game, with argument checks and rate limits |
Net.ts |
The typed remote wrappers, the check helpers and the rate limiter |
Attributes.ts |
Names of the player attributes the server sets for the UI |
Items.ts |
Item instance types, item origins, merge order, item bonuses |
Odds.ts |
Weighted odds, rolling, and percentage formatting |
Gacha.ts |
Egg and wheel odds with luck, pity and once-only items, and egg prices |
Prices.ts |
Robux prices from GetProductInfoAsync, cached |
LiveEvents.ts |
Which EVENT_MULTIPLIERS are running |
EventBus.ts |
A small typed event bus |
Time.ts |
UTC day and week helpers |
demo/ |
Demo hub layout (layout.json, Layout.ts), pet looks, the demo season and the demo's one remote. See The demo world |
src/server
One module per system. Each exports a start(), and Main.server.ts calls them in order:
Policy.start();
Multipliers.start();
Gamepasses.start();
Currency.start();
Boosts.start();
Inventory.start();
Purchases.start();
// ...
Season.start();
Rebirth.start();
Admin.start();
PlayerData.start();
PlayerData.start() goes last. Systems register PlayerData.onLoad handlers inside their own start(), so by the time anyone's data loads, every system is listening. If you add a system, call its start() above PlayerData.start().
Subfolders:
server/data/:PlayerData.ts(load, save, session lock),Schema.ts(saved shape, defaults, migrations),Retry.ts(retries with backoff).server/social/:Group.ts,Friends.ts,Referrals.ts.server/trading/:Trading.ts(sessions and the commit),Rules.ts,TradeLog.ts,History.ts,Switch.ts.server/demo/: builds the demo hub (see The demo world).
server/CodesConfig.ts holds the promo codes. It's on the server so players can't read the list.
src/client
Main.client.tsmounts the UI, starts audio and chat tags, and opens the daily reward window once data loads.State.tsturns player attributes into Fusion values and holds the window manager (State.panels).Theme.tshas every color, font, size and timing the UI uses.ui/kit/is the component kit: buttons, windows, cards, bars, tabs, toasts, tooltips.ui/*.tsis one file per window (Shop, Eggs, Quests, SeasonPass and so on), plusHud.ts, which builds the HUD and mounts every window.Marketplace.ts,Audio.ts,Onboarding.ts,ChatTags.ts,Claimable.ts,Synced.tsandInventory.tsare client helpers.demo/: client side of the demo hub (pickup text, prompts, pets, boards).
The demo world
DEMO.enabled in Config.ts (true by default) makes the server build a small hub with a stand for each system. The code is in src/server/demo/, src/client/demo/ and src/shared/demo/, and Main.server.ts and Main.client.ts start it on lines marked // demo. Nothing else depends on it. The demo has one remote of its own, in shared/demo/Remotes.ts; it's the only remote outside shared/Remotes.ts.
To remove it for your own game, delete those three folders, the DEMO block at the end of Config.ts, and the // demo lines in the two Main files. To keep the code but not build the hub, set DEMO.enabled to false.
How state reaches the UI
The server owns all player data. When a player joins, PlayerData loads their save and runs every onLoad handler. Those handlers set attributes on the Player, using the names in shared/Attributes.ts:
export const Attr = {
/** True once the player's saved data has loaded. */
Loaded: "DataLoaded",
/** The coins balance. Same as currencyAttr("coins"). */
Coins: "Currency_coins",
DailyStreak: "DailyStreak",
// ...
Attributes replicate to the client on their own. client/State.ts wraps each one in a Fusion value:
export const loaded = attribute(Attr.Loaded, false);
export const coins = attribute(Attr.Coins, 0);
export const dailyReady = attribute(Attr.DailyReady, false);
So when the server changes a balance, the HUD updates with no extra remote. Larger state that doesn't fit in an attribute (the inventory, quest boards, the index, the season pass) goes over a serverEvent such as InventoryChanged or QuestsChanged, and the client asks for a first copy with a clientFunction such as GetInventory.
The client asks the server to do things through the remotes in shared/Remotes.ts. The server checks every argument and rate-limits every remote before a handler runs (see Remotes).
Game events
server/Events.ts is an in-process event bus for things that happen in the game: currencyEarned, itemAdded, eggOpened, wheelSpun, rebirthed, questClaimed, purchase and others. Systems fire them, and quests, achievements and the index listen. server/Goals.ts turns these events into quest and achievement progress.