# RBLX Essentials 3.0: work on a game with your AI assistant

Applies to Tools 3.1.3, Kernel 3.0.1 and the nine RBLX Essentials packs of release 3.1.3, Easy Pet System 3.0.3 among them (section 11 names each).

**Read this file whole, word for word.** It is long and its code blocks matter. A web-fetch tool that summarizes pages, or limits quotes, loses them: download the raw file instead (for example `curl -s https://rblxessentials.com/AI-START-HERE.md -o AI-START-HERE.md`) and read that copy.

This guide is for an AI assistant that works in Roblox Studio through the Studio MCP server (the tools `list_roblox_studios`, `get_studio_state`, `execute_luau`, `multi_edit`, `start_stop_play` and `get_console_output`), with those tools only. Code blocks marked `-- Edit:` run with `execute_luau` in the Edit DataModel; `-- Play, Server:` and `-- Play, Client:` blocks, in that DataModel while the game plays.

## 1. Rules

1. **Pick the Studio first.** Call `list_roblox_studios`, use the `id` of the place the user named as `studio_id` in every call, and check `get_studio_state` before you edit: edits need the mode `Edit`.
2. **One Tools operation that changes the place per `execute_luau` call.** Each call is one undo step (Ctrl+Z reverts it whole). Read-only queries (`T.describe`, `T.config.where`, `T.config.check`, `T.validate`) change nothing, so several may share a call when the answer stays short (rule 5).
3. **Never assign a script's `Source` from `execute_luau`.** Create and change scripts only with `multi_edit`.
4. **Apply each pending action once, unchanged, and only while its precondition holds.** When the task asks for values of the game's own, add them afterwards with a second `multi_edit` (section 5): the first call is exactly the text Tools offered (in a migration, the text the next `T.migrate()` checks the module against), the second the game's change, so a mistake in either shows and undoes alone. A script Tools needs comes as a `multi_edit` call with a `precondition`: the module must still be missing (on an existing module, an empty `old_string` does not fail: it puts the text in front of what is there).
5. **Return a short string from `execute_luau`.** The Studio MCP cuts an answer after 100,000 characters, and a cut one cannot be read. Use `T.format(result)` for a summary, or encode (`HttpService:JSONEncode`) only the fields you act on: for `T.migrate()`, section 8's answer; for `T.describe()` or `T.validate()`, one part at a time (`T.describe().data.migration`, `T.describe({ pack = "<PackId>" })`).
6. **Stop Play before editing.** The Edit DataModel is not reachable while the game runs.
7. **Never wipe, reset or delete saved player data (Studio or live), backups, or anything else the user cannot recreate, unless the user asks for that in this conversation.** Not even to test a new player, or when nobody can answer.
8. **If a call never returns,** Studio may be paused on an error ("Exception Hit"): ask the user to press Resume; never repeat the call.
9. **Do not save or publish** the place unless asked.
10. **After `T.migrate()` is done, quote its `report` whole in your reply**, under what you changed (section 8).
11. **Use `force` only where this guide allows it, and only once the user agrees.** Where this guide says a code is never forced, that beats a `fix` that offers `force` (Tools writes one fix for all): never force `kernel-major-mismatch`, a `kernel-major-upgrade` in a place with any RBLX Essentials 2.x pack, or a refusal section 8 or 10 says `force` never lifts.

## 2. What lives where

```text
ReplicatedStorage/RBLXEssentials/
  Kernel/                    product: replaced by updates, never edit it
  Packs/<PackId>/            product: manifest, Schema, Pieces, Client, Migrations
  Config/<PackId>            the GAME's: options clients may read
ServerScriptService/RBLXEssentials/
  Kernel, Packs/<PackId>     product
  Config/<PackId>            the GAME's: server-only options
StarterGui/RBLXEssentials/
  <PackId>/                  product: each pack's default interface pieces
  Interface/                 the GAME's: pieces that replace the defaults
  Interface/Custom/          the GAME's: kept by a migration, not managed
ServerStorage/RBLXEssentials/
  Tools                      product: the operations library
  Content/<ContentId>/       the GAME's: themes and other content, installed
  Backups/<date>-<reason>/   the GAME's: copies made before anything is replaced
```

Updates replace the product zones whole and never touch the game's. To change a pack's interface, fork its piece into `Interface` (section 6); to change its behaviour, set its configuration (section 5).

## 3. Tools

Every operation is in one library:

```lua
-- Edit: what is installed, what waits to be installed, and what is wrong
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return T.format(T.describe({ format = "text" }))
```

Each operation returns `{ ok, operation, changes, issues, data?, text? }`; `ok` is false only when an issue has severity `error`. Each issue has a `code`, `severity`, `target`, `message` and `fix`: when something fails, do what its `fix` says (within rule 11 for `force`). The main operations:

| Operation | What it does |
|---|---|
| `T.describe({ format = "text" })` | Packs and versions, pending containers, configuration, pieces, content, problems; `{ pack = "CodesSystem" }` for one pack. |
| `T.install(ref)`, `T.update(ref)`, `T.uninstall(packId)` | Install or update a pack from a container the user dragged in, or from the website with `{ website = "<PackId>" }`, or remove one. |
| `T.config.where(packId)`, `T.config.reference(packId)`, `T.config.check(packId)`, `T.config.create(packId)` | Where a pack's configuration lives, every option, whether it is valid, its modules from the template. |
| `T.content.list()`, `T.content.activate(contentId, { piece = id })`, `T.content.fork(pieceId)`, `T.content.deactivate(id)` | Themes and their pieces (section 6). |
| `T.validate()` | Everything checked, before Play. |
| `T.migrate()` | A 2.x place to 3.0 (section 8). `T.migrate({ dryRun = true })` checks it and changes nothing. |
| `T.backups.list()` | The backups (section 9). |

`ServerStorage.RBLXEssentials.Tools` appears once the place has a pack and the plugin is installed; if missing, ask the user to install the plugin and a first pack from its card (**My packs**, **Install**), or to drag one in and press Rescan.

## 4. Install a pack

The user's way first: the plugin card's **My packs**, **Install** on the pack. It installs the packs it needs first and writes its config modules, so nothing is left to apply; `T.describe` then lists it as installed.

With the RBLX Essentials Plugin 3.1 or newer installed in this Studio, a pack can come straight from the website through you too: `T.install({ website = "CodesSystem" })` (or `T.update`). Free packs need no sign-in; a paid pack must be owned by the account signed in on the plugin's Account tab. Install dependencies first, the same way.

Or the user drags a `.rbxm` in (you cannot drag files into Studio). `T.describe` then lists it as a pending pack, such as `Pending pack: CodesSystem 3.0.0 at Workspace.RBLXE_Pack` (when several wait, each reference ends in `#<PackId>`).

First look at what the place already has:

- If `T.describe` warns `migration-available`, the place runs a 2.x version of a pending pack: that is a migration (section 8), which `T.install` and `T.update` refuse.
- Otherwise (no RBLX Essentials yet, or a pack the place does not have), install each pending pack by the reference `T.describe` printed, dependencies first (install the packs `T.describe` lists as pending: a pack that is not in the place is not part of the job, even when this guide names it):

```lua
-- Edit: install the pack container waiting in Workspace
local T = require(game.ServerStorage.RBLXEssentials.Tools)
local result = T.install("Workspace.RBLXE_Pack")
return game:GetService("HttpService"):JSONEncode(result)
```

If `ok` is false, nothing was installed: do what each error's `fix` says (rule 11 for `force`). The refusals you will meet:

- `missing-dependency`: the pack needs another one installed first. Install DatastorePurchaseSystem first: every pack that saves player data needs it. If `T.describe` lists it neither as installed nor as pending, install it with `T.install({ website = "DatastorePurchaseSystem" })` (or the user presses Install on it in the card, or drags its `.rbxm` in), then this one again.
- `kernel-major-upgrade`: the place's packs run on a 2.x Kernel and this pack brings Kernel 3.0. With any RBLX Essentials 2.x pack in the place, every pack moves together with `T.migrate()` (section 8), never by installing over it. Only in a place whose packs are all the game's own, and once the user agrees, use `T.install("<reference>", { force = true })` (it moves the 2.x Kernel to `Backups`, and reports what it lifted as warnings).
- `kernel-major-mismatch`: a 2.x pack onto a place with Kernel 3.0. Ask the user to drag in its 3.0 `.rbxm` instead (every RBLX Essentials pack has one), and never force it, even though its `fix` offers `{ force = true }`.
- `already-installed`: the pack is there already: a 2.x version moves to 3.0 only with `T.migrate()` (section 8), a 3.x one updates with `T.update("<reference>")`.

A pack with configuration needs its config modules: Tools does not write scripts from the MCP, so the result lists them as changes with `pending = true`, each with an `action`:

```text
{ tool = "multi_edit", file_path = "game.ServerScriptService.RBLXEssentials.Config.CodesSystem",
  className = "ModuleScript", edits = { { old_string = "", new_string = "<the module text>" } },
  precondition = "... does not exist: check it right before applying ..." }
```

For each one, check that the module is still missing:

```lua
-- Edit: where Codes System's configuration lives, and whether each module exists yet
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return game:GetService("HttpService"):JSONEncode(T.config.where("CodesSystem").data)
```

When `exists` is `false` for that side (`shared` is the ReplicatedStorage module, `server` the ServerScriptService one), call `multi_edit` once with `studio_id`, `datamodel_type = "Edit"` and the action's `file_path`, `className` and `edits`, exactly as given. One `T.config.where` check may cover several modules (one `execute_luau` call may hold a `T.config.where` for each pack) right before their `multi_edit`s, when no other call runs between them. If you lost the actions, `T.config.create("CodesSystem")` returns them again for the modules still missing, as the commented template: Codes System's example codes are stated only by the install's own action.

## 5. Configuration in code

Each pack's options live in two modules the game owns: `ReplicatedStorage.RBLXEssentials.Config.<PackId>` (options clients may read) and `ServerScriptService.RBLXEssentials.Config.<PackId>` (options marked server-only). Each returns only what the game changes; the rest keeps its default. A template module lists every option of its side, with its doc and default, commented out.

A group (its template line says `group`, such as Roulette System's `Spins` or Notifications System's `Triggers`) merges key by key, as a `map` does: replace the group's commented block with only the keys you change, and the others keep their defaults (in a group the template states because a key is pinned, such as `DevProducts`, change only the lines of your keys). For three starting spins, the five commented lines of `Spins` in `ReplicatedStorage.RBLXEssentials.Config.RouletteSystem` become these three:

```text
	Spins = {
		StartingSpins = 3,
	},
```

`FreeSpinCooldown` and `MaxSpins` keep their defaults. A `list` or a `tree` replaces its default whole: write every entry (an entry of a list that is a group still gets the defaults of the keys it leaves out; a tree's nested tables get nothing they leave out). So DatastorePurchaseSystem's `DefaultData` keeps `Cash` and `Wins` in `Stats` only if you write them: to add a stat, add its line inside `Stats` and keep theirs (with `DefaultData = { Stats = { Level = 1 } }`, new players start with no `Cash` or `Wins`, and neither `T.validate` nor `T.config.check` reports it).

**The second currency is `Diamonds`.** The packs have two currencies, `Stats.Cash` and `Stats.Diamonds`: Time Rewards (its default gifts and days), Roulette (the starter wheel's +10 slot) and Codes (`Type = "Diamonds"`) grant Diamonds, and the first grant creates the value (to have it from a player's first join, write `Diamonds = 0` in `DefaultData`'s `Stats` beside `Cash` and `Wins`). When the user says gems, premium currency, a second currency or similar, use `Diamonds`: never add a parallel stat such as `Stats.Gems`, which no pack grants. To give it another name, change only what players see, never the data key: the player list's column (Easy Leaderstats' `Leaderstats` entry `{ DataKey = "Stats.Diamonds", Name = "Gems" }`, with the whole list written), the `Icon` of Time Rewards' gifts and days and the `Wheel` labels of Roulette (`DisplayName`, `Icon`), and the game's own interface. No pack's HUD shows Diamonds.

To see every option at once:

```lua
-- Edit: every option of Codes System, with its type, default and rules
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return T.config.reference("CodesSystem").text
```

Codes System takes two options:

- `Codes` (server only: clients never receive it), the list of codes, each `{ Code, Type, Target?, Amount, Expires? }`:
  - `Type` is `Cash` or `Diamonds` (added to `Stats.Cash` or `Stats.Diamonds`), `Spins` (needs Roulette System), `Stat` (added to the data path in `Target`, such as `Stats.Wins`) or `Custom` (runs the module named in `Target`: the game's own in `ServerScriptService.RBLXEssentials.Extensions.CodesSystem` first, else the pack's `CustomRewards`, such as `GrantPet`);
  - `Target` is required for `Stat` and `Custom`, and left out for the other types;
  - a code grants one `Type` once. For "a code that gives cash and gems", make it one `Custom` code whose module (in `ServerScriptService.RBLXEssentials.Extensions.CodesSystem`, returning `{ Grant = function(player, data, amount) ... end }`) adds to `data.Stats.Cash` and `data.Stats.Diamonds` and returns true; two codes with different names give two rewards for two redemptions;
  - `Amount` is any number. A negative `Amount` is valid, as in 2.x: it takes away from `Cash`, `Diamonds` or a `Stat`, never below 0 (a negative `Spins` grants nothing; a `Custom` module gets the number as it is). A migrated negative `Amount` is the game's choice: keep it;
  - `Expires`, optional, is a last date, `YYYY-MM-DD` in UTC (the code stops at 00:00 UTC that day);
  - each code appears once, ignoring case and surrounding spaces.
- `Messages` (shared): the texts the panel shows. Set only the ones you change.

A server module with codes can look like this:

```lua
-- ServerScriptService.RBLXEssentials.Config.CodesSystem
return {
	Codes = {
		{ Code = "LAUNCH", Type = "Cash", Amount = 500 },
		{ Code = "SUMMERGEMS", Type = "Diamonds", Amount = 25, Expires = "2026-12-31" },
		{ Code = "LEVELUP", Type = "Stat", Target = "Stats.Wins", Amount = 1 },
		{ Code = "PETDAY2027", Type = "Custom", Target = "GrantPet", Amount = 1 },
	},
}
```

A fresh install's server module states the example codes (section 11); to make one the game's own, edit its lines with `multi_edit`:

```json
{
  "studio_id": "<studio_id>",
  "datamodel_type": "Edit",
  "file_path": "game.ServerScriptService.RBLXEssentials.Config.CodesSystem",
  "edits": [
    {
      "old_string": "\t\t\tCode = \"WELCOME2027\",\n\t\t\tExpires = \"2027-12-31\",",
      "new_string": "\t\t\tCode = \"LAUNCH\","
    }
  ]
}
```

`old_string` must appear exactly once in the module; the person may also change settings in the plugin, so read its text first (`return game.ServerScriptService.RBLXEssentials.Config.CodesSystem.Source` in an Edit call) and pick one from it. Then check the configuration:

```lua
-- Edit: is Codes System's configuration valid?
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return T.format(T.config.check("CodesSystem"))
```

An invalid configuration names the option, for example `Config.CodesSystem.Codes[2].Target: is required when Type is Stat`. The pack does not start in the game until it is valid.

## 6. Interface pieces and themes

A pack ships a default copy of each piece of interface it drives (Codes System: `CodesSystem.CodesPanel` and its HUD button, `CodesSystem.CodesButton`). A theme in the place's content library may have its own copy of a piece; activated, it goes to `Interface`, and the game uses it while it satisfies the piece's contract (otherwise the pack's default).

```lua
-- Edit: the themes in this place's content library, with their pieces
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return game:GetService("HttpService"):JSONEncode(T.content.list().data)
```

Each entry has an `id` and its `pieces`. A request that names a piece of a theme, such as "use the theme's Codes panel instead of the pack's own", is always `{ piece = ... }`; activate a whole theme only when the user asks to switch the game to it ("use the <ThemeId> theme", "make it the game's theme"). Replace `<ThemeId>` with the theme's `id`:

```lua
-- Edit: use one theme's Codes panel instead of the pack's own
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return T.format(T.content.activate("<ThemeId>", { piece = "CodesSystem.CodesPanel" }))
```

Without `{ piece = ... }`, `T.content.activate` activates every piece of the theme and makes it the preferred theme, whose pieces later installs and updates activate too (`T.content.deactivate("<ThemeId>")` undoes both). `T.content.fork("CodesSystem.CodesPanel")` copies the pack's own piece into `Interface` for the game to edit; `T.content.deactivate("CodesSystem.CodesPanel")` goes back to the pack's piece. Never edit a pack's default piece in place: the next update replaces it.

Studded UI is General UI Kit's free theme (`StuddedUI`). The user installs it from the plugin card's **Themes** (**Install** puts it in and applies it). If they drag its `.rbxm` in instead, `T.content.install(workspace.RBLXE_Content)`, then `T.content.activate("StuddedUI")` restyles every kit menu and button, and the Codes, Spin, Rewards, DailyRewards and Pets menus of their packs; `T.content.deactivate("StuddedUI")` goes back (its copies go to `Backups`).

## 7. Validate, then test in Play

```lua
-- Edit: check the whole place before pressing Play
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return T.format(T.validate())
```

Fix every error (section 10 says what each code needs). `codes-example-live` (warning): Codes System's example codes are live in any game that keeps them; list each to the user before publishing, as the demo ids. `demo-prop-live`: a demo pad a pack placed in the world (section 11), a warning when it gives something on a live server; list each to the user before publishing. `robux-egg-no-product` (warning): a Robux egg whose `productId` is missing, 0 or below 0 cannot be bought (section 11); tell the user. `kernel-major-mismatch` as a warning: a 2.x pack still runs on Kernel 3.0; migrate it (section 8). `duplicate-panel-id` as information names a panel a 3.0 pack ships hidden behind another pack's piece by design (Sound System's Settings, Easy Pet System's Shop): nothing to do, never edit or remove it; as a warning or an error, do what its `fix` says.

Then start the game with `start_stop_play` (`is_start = true`) and wait until `get_studio_state` lists the `Server` and `Client` DataModels. Check that the packs started:

```lua
-- Play, Server: wait for the Kernel's boot, then list the packs that did not start
local sss = game:GetService("ServerScriptService"):WaitForChild("RBLXEssentials")
local deadline = os.clock() + 30
while not sss:GetAttribute("RBLXE_ServerBooted") and os.clock() < deadline do
	task.wait(0.2)
end
return tostring(game:GetService("ReplicatedStorage").RBLXEssentials:GetAttribute("RBLXE_ServerReport"))
```

`{"failed":[],"skipped":[]}` means every pack started; otherwise each entry says why, briefly (`invalid configuration (...)`, or that the pack errored while starting): the details are in the server's Output (`[RBLXE] <PackId> failed:`).

Read the Output with `get_console_output` once the server has booted, even when every pack started. Before you tell the user that no warning is left, start Play again after your last change and read that session's Output: never answer from an earlier session's. RBLX Essentials' lines start with `[RBLXE]`, `[RBLXE <part>]`, a pack's id (`[<PackId>]`) or `[DataStore]`, `[PurchaseManager]`, `[StudioSafeId]`, `[EasyLeaderstats]`, `[GlobalLeaderboards]`, `[DataBind]`, `[SFXSystem]`, `[GrantPet]` or `[RandomPet]`; any other line comes from another Studio plugin or from the game's own scripts. The lists below say what RBLX Essentials' other lines need. Another print is information, such as `[RBLXE] UI: <n> pieces: <n> from Interface, <n> default`, `[RBLXE] UI: <PieceId> <- Interface (<source>)`, `[RBLXE] <side> booted: <packs>` and `[NotificationsSystem] ready. Triggers on: <names>`; another warning is not: tell the user what it says.

These need nothing:
- `[CodesSystem] no codes yet`: add codes only when the task asks (section 5). If `ServerScriptService.RBLXEssentials.Config.CodesSystem` does not exist, the install's pending `multi_edit` was not applied (section 4): until a module exists, the game has no codes.
- `[DataStore] Studio API access unavailable (place not published, or Studio API access disabled). Player data will NOT persist this session.`: the place is not published, or it is and its Studio API access is off, so every Play session starts with fresh data.
- `[RouletteSystem] Rewards is not set in ServerScriptService.RBLXEssentials.Config.RouletteSystem, so the wheel is the starter wheel`: the shipped wheel, the seven 2.x slots (section 11).
- `[RandomPet] Slot <n> (Random Pet) will grant its fallback (<amount> <stat>) because Easy Pet System is not installed`: that slot grants its fallback instead of a pet.
- `[TimeRewardsSystem] <slot> <why>, so it grants its Fallback (<reward>) instead`: it does, until its pack is installed.
- `[RBLXE] UI: piece <PieceId> has no decision from the server (no installed pack declares it, or no copy of it is usable)` (client) for a General UI Kit stand-in (`GeneralUIKit.CodesButton`, `SpinButton`, `RewardsButton`, `DailyRewardsButton`, `PetsButton` and their panels) whose pack is installed: that pack's own piece is used, by design. Do not explain it to the user.
- `[RBLXE] UI: <tag> '<name>' <copy> deposed by the 3.0 piece <PieceId>` and `[RBLXE] UI: <tag> '<name>' <copy> left unadopted: the slot belongs to the 3.0 piece <PieceId>` (client) when `<name>` is a PanelId a `duplicate-panel-id` information names (or `TogglePanel:<PanelId>`, the button that opens it): a pack's own copy, hidden by design behind another pack's piece.

These need the user. List each one in your reply as an action for the user before publishing:
- `[EasyPetSystem] FreeMode is on in ReplicatedStorage.RBLXEssentials.Config.DatastorePurchaseSystem`: in 3.0 it works only in Studio, and not while Studio uses the live store; a live server checks real ownership and warns at start until it is `false`. Tell the user; they set it `false` before publishing if the game does not want it in Studio tests either.
- `[EasyPetSystem] Gamepass ID for '<Pass>' is still a default placeholder`, `[EasyPetSystem] Dev product ID for Robux egg '<EggId>' is still a default placeholder` (section 11: it comes from the egg's `PetData` entry, so deleting only the stand leaves it) and `[RouletteSystem] DevProducts.<Name> is still the RBLX Essentials demo product (<id>)`: each names an id only the user can supply.
- `[EasyPetSystem] Gamepass '<Pass>' is 0, so that pass is off` and `[EasyPetSystem] Robux egg '<EggId>' has productId 0, so it shows but cannot be bought`: there is no id to ask for; tell the user that pass is off, or that egg cannot be bought, until the game sets its own id (for the egg, section 11 says what to do).
- `[RouletteSystem] DevProducts.<Name> is 0, so the +<n> spin button is shown but sells nothing`: tell the user it sells nothing until the game sets its own id.
- `[GrantPet] Easy Pet System v2 not found. Install it, or change this <gift's, code's or slot's> Type`: Time Rewards System and Codes System say it when the server starts and disable that gift or day (or grant its `Fallback`), or that code (it answers `CodeDisabled`); Roulette System says it when that slot lands, and grants nothing.
- `[TimeRewardsSystem] <Day> grants Spins but Roulette System is not installed, so it is disabled` and `[TimeRewardsSystem] the daily cycle stops at Day <n> for every player who reaches it: that day can never be granted`: a `Spins` day needs Roulette System and a `GrantPet` day needs Easy Pet System (a migrated game keeps its 2.x days). Without that pack a day with no `Fallback` is disabled and the daily cycle stops there for every player who reaches it: tell the user, who decides whether to install that pack or change that day (`DailyRewards.Days`, never on your own). The daily-cycle line names the first disabled day, whatever disabled it: also a day that is off (`Enabled = false`; `[TimeRewardsSystem] <Day> is off` says so too) or whose Custom module is missing, has no `Grant` (a line names the module) or has a `Prepare` that fails or returns false (no other line): tell the user the same way.

Check which Codes panel the player sees:

```lua
-- Play, Client: which Codes panel the menu uses, and where it came from
local gui = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui")
local deadline = os.clock() + 30
while not gui:GetAttribute("RBLXE_ClientBooted") and os.clock() < deadline do
	task.wait(0.2)
end
local shell = gui:WaitForChild("RBLXEssentialsGUI", 30)
local menus = shell and shell:WaitForChild("Menus", 30)
if menus == nil then
	return "no RBLXEssentialsGUI.Menus: the Kernel's client did not start its interface; read the Output with get_console_output ([RBLXE] lines)"
end
for _ = 1, 150 do
	for _, panel in ipairs(menus:GetChildren()) do
		if panel:GetAttribute("RBLXE_Piece") == "CodesSystem.CodesPanel" then
			return ("in use: %s, from %s"):format(panel:GetFullName(), tostring(panel:GetAttribute("RBLXE_Source") or "the pack itself"))
		end
	end
	task.wait(0.2)
end
return "no Codes panel was adopted: read the Output with get_console_output ([RBLXE] UI lines)"
```

A theme's piece says `from <ThemeId>@<version>`. Then redeem a code the way the panel does, and see what it gave:

```lua
-- Play, Client: redeem a code the way the panel does, and show the Cash it gave
local player = game:GetService("Players").LocalPlayer
local data = player:WaitForChild("PlayerData", 30)
local stats = data and data:WaitForChild("Stats", 10)
local cash = stats and stats:WaitForChild("Cash", 10)
local remotes = game:GetService("ReplicatedStorage").RBLXEssentials.Kernel:WaitForChild("Remotes", 30)
local folder = remotes and remotes:WaitForChild("CodesSystem", 30)
local redeem = folder and folder:WaitForChild("Redeem", 30)
if cash == nil then
	return "not ready: no PlayerData.Stats.Cash (is DatastorePurchaseSystem running? see the Play, Server snippet)"
elseif redeem == nil then
	return "not ready: no CodesSystem Redeem remote (did Codes System start? see the Play, Server snippet)"
end
local before = cash.Value
local answer = redeem:InvokeServer("LAUNCH")
local deadline = os.clock() + 5
while cash.Value == before and os.clock() < deadline do
	task.wait(0.1)
end
return game:GetService("HttpService"):JSONEncode({ answer = answer, cashBefore = before, cashAfter = cash.Value })
```

`answer.Success = true` and the Cash grown by the code's `Amount` prove it. A failed answer's `Error` is `NoSuchCode` (not in the config), `AlreadyRedeemed` (this player used it; in an unpublished place each Play session starts with fresh data), `Expired`, `CodeDisabled` (its Custom module is missing or not ready), `TooFast` (wait a second between tries), `NoData` (the player's data is still loading: try again) or `GrantFailed`. A `Diamonds` or `Stat` code adds to that value under `PlayerData`; a `Spins` code adds to `PlayerData.Roulette.Spins`.

The other packs, each in a `Play, Client` call written like the one above (wait for `PlayerData` and the remote first). `Remotes` is `game.ReplicatedStorage.RBLXEssentials.Kernel.Remotes`; `PlayerData` mirrors the player's save as folders and values (a list's entries are named `[1]`, `[2]`...):

| Pack | Do | It works when |
|---|---|---|
| DatastorePurchaseSystem | Read `PlayerData.Stats`. | `Cash`, `Wins` and every `DefaultData` key are there; `Diamonds` once something granted it, or from the start if `DefaultData` writes it. |
| EasyLeaderstatsSystem | Read `LocalPlayer.leaderstats`. | One value per `Leaderstats` entry, named by its `Name`, equal to the value its `DataKey` names under `PlayerData`. |
| RouletteSystem | `Remotes.RouletteSystem.RequestSpin:InvokeServer()` | `{ Success = true, Reward = { Index, DisplayName, Icon } }` and `PlayerData.Roulette.Spins` one lower; the reward lands after `Remotes.RouletteSystem.AnimationComplete:FireServer()`, or 10 seconds later. `Error = "No spins available"`: none left. |
| TimeRewardsSystem | `Remotes.TimeRewardsSystem.ClaimDaily:InvokeServer()`; `ClaimGift:InvokeServer(n)` for session gift n. | `{ Success = true, Daily = ... }` (a gift: `Session = ...`) and the reward added. `Error`: `OnCooldown` (claimed this cycle), `DayDisabled`, `NoData`; a gift: `NotReady` (its minutes have not passed), `AlreadyClaimed`. |
| EasyPetSystem | `Remotes.EasyPetSystem.BuyAndHatchEgg:InvokeServer({ eggId = "Tier1Egg", count = 1 })` | `{ ok = true, pets = { { id, rarity, ... } } }`, the egg's price taken from `Stats.Cash` and the pet added to `PlayerData.Pets.Owned`. `reason`: `insufficient_funds` (give Cash first, as with the code above), `cooldown`, `inventory_full`, `need_triple_pass` (count 3), `invalid_egg` (not a Cash egg). |
| SoundSystem | Read `game.SoundService.RBLXEssentials.SoundSystem.Music.ActiveTrack`. | `IsPlaying` is true: the music started on its own. |
| GeneralUIKit, NotificationsSystem | The `Menus` check above with another PieceId (`GeneralUIKit.ShopPanel`); the server's `[NotificationsSystem] ready. Triggers on: <names>` line. | The panel is in use; the triggers named are the ones `Triggers` turns on. |

Stop the game with `start_stop_play` (`is_start = false`) when you are done.

## 8. Move a 2.x place to 3.0

When a place still runs 2.x packs and the user drags in their 3.0 containers, `T.describe` warns `migration-available`. Use `T.migrate()`: `T.install` refuses an installed pack (`already-installed`), and `T.update` refuses a 2.x pack, even with `force` (`update-needs-migrate`, with `kernel-major-upgrade` pointing to `T.migrate()`): it would drop the 2.x Settings. Every installed RBLX Essentials pack moves to 3.0 together, and each ships a 3.0 version, so a 2.x place migrates once the user drags in the 3.0 container of every RBLX Essentials pack it has. A missing one stops it (`migrate-missing-containers`, named): ask the user for that `.rbxm`; never work around it with `T.update`, `force` or by removing a pack.

```lua
-- Edit: move a 2.x place to 3.0, and answer only what to act on
local T = require(game.ServerStorage.RBLXEssentials.Tools)
local result = T.migrate()
local data = result.data or {}
local answer = { ok = result.ok, phase = data.phase, force = data.force, report = data.report, issues = {}, pending = {}, texts = {} }
answer.next = data.report and "Quote report whole in your reply, under what you changed."
-- The two reports whose message needs nothing after a migration.
local brief = { ["product-code-unverified"] = true, ["product-content-unverified"] = true }
for _, issue in ipairs(result.issues) do
	local whole = not brief[issue.code] and (issue.severity ~= "info" or string.sub(issue.code, 1, 6) == "reads-")
	table.insert(answer.issues, { severity = issue.severity, code = issue.code, target = issue.target,
		message = if whole then issue.message else nil, fix = issue.fix, force = issue.force })
end
for _, change in ipairs(result.changes) do
	if change.pending then
		table.insert(answer.pending, change.action)
	end
end
if not result.ok then
	for packId, pack in pairs(data.packs or {}) do
		for side, module in pairs(pack.config or {}) do
			answer.texts[packId .. "." .. side] = module.text
		end
	end
end
return game:GetService("HttpService"):JSONEncode(answer)
```

A finished run (`done`) has `report`: what the migration did, first one line for the runs (the run before the last one only backed up the place's RBLX Essentials and converted the 2.x Settings into the config texts; the last one moved the packs: describe the runs as that line does), then the first sentence of each `migration-action` message, then of each `migration-note` message, as Tools wrote it after the pack and its 2.x setting (the answer's issue holds the whole message and its `fix`; a note T.migrate decided itself ends with what it found, in parentheses, and so does a note about a module it carried to `Extensions`: whether that module reads `ctx.settings` or its old parent), then, pack by pack, the pieces it forked into `Interface` (the copy players saw, and what the game deleted, kept hidden, with when its pack still shows it), the content it kept live (the game's changes, and what Tools cannot compare, kept as 2.x had it, unverified) or kept removed (what the game removed, and what Tools only counts as removed), the modules it carried to `Extensions` (a trigger switched off, as the game had it, says so: Play does not prove it) and what it kept in `Interface/Custom`.

Return this answer, never the whole result (rule 5). Show the user the messages it keeps: each error and warning that needs something, and each `reads-2x-settings`, `reads-ctx-settings` and `reads-script-parent`. When the run refused, `texts["<PackId>.<side>"]` holds each converted config text (the result's `data.packs.<PackId>.config.<side>.text`). After each run, check `T.describe().data.migration` (encoded): its `state` is `awaiting-config` after the first run and `done` after the second, `backup` names the migration backup, and once it is `done`, `report` holds the finished run's lines again, unless a later run finished it (`migrate-finished`, after a run that stopped once its updates were done): then there is no `report`, and what to show the user is in the answer of the run that stopped (its `migration-action` and `migration-note` messages). There is none either after a `follow-up-failed` that says the report could not be kept on the migration backup: quote the `report` of that run's answer. If an answer is cut or cannot be read, check that first, and do not run `T.migrate()` again only to see its result.

It checks everything first and changes nothing if anything is wrong, listing every refusal of every pack at once. From the MCP it takes two runs:

1. **The first run** copies the place's RBLX Essentials into a protected `Backups/<date>-migration`, converts each pack's 2.x Settings (only what the game had changed, read exactly as 2.x did), stores the texts in that backup and returns the config modules as pending `multi_edit` actions. It ends with the warning `migrate-config-pending` and `data.phase = "awaiting-config"`; nothing else changes yet (the 2.x packs keep running). Each text starts with a `--rblxe-migrated v1 ...` line recording the values it was converted from: keep it in the module while the migration is unfinished.
2. **Apply each action once**, as in section 4, checking first that the module is still missing. The texts hold the game's 2.x values: add nothing, and change a module only once `T.migrate()` reports `done` (a changed migrated value refuses the next run). If you lost the actions, `T.config.create("<PackId>")` offers the stored text again.
3. **Run `T.migrate()` again.** It converts the 2.x Settings again and checks the modules against that. If the game changed them since (removed, disabled or changed a code, reverted a message), a module made from an earlier text (its `--rblxe-migrated` line records what it held) would bring the old value back: it refuses with `migrate-config-stale`, names each 2.x slot and hands the new text over (the answer's `texts`). Ask the user whether to delete that module and run `T.migrate()` again (it offers the new text) or to change the module so it no longer holds the old values; `force` never lifts this. A refusal the first run's `force` lifted (`migrate-custom-modified`, `migrate-custom-unknown`, `migrate-config-differs`) comes back while it still holds: show it to the user again, and pass this run's own `data.force` only once they agree. Once the modules pass, it moves each pack: installs it, forks the interface the game edited into `Interface`, keeps the game's own interface in `Interface/Custom` and carries its modules to `Extensions`; a step that fails puts that pack back. Then it runs `T.validate()`, and `data.phase` is `"done"`.
4. **Quote `report` whole in your reply**, under what you changed: its lines as they are, not a summary.

A finished run (`done`) also reports what it did, and each of these codes needs only this:
- `migration-action`: a 2.x setting that still needs the game's action, such as Roulette System's demo product ids. Show the user its line in `report` (the rest of its message is in the answer if the user asks) and do what its `fix` says, asking the user first where an edit or a value is theirs to decide: do it, or tell the user why you did not (such as the user's own product ids), before you report.
- `kernel-major-upgrade`, as information: the run moved every pack to Kernel 3.0 itself. Nothing to do.
- `product-code-unverified` and `product-content-unverified`: the 2.x packs carried no fingerprints, so Tools could not compare the code and content the update replaced; the replaced copy is in the migration backup and in that pack's update backup (`Backups/<date>-update-<PackId>`, which the `product-content-unverified` message names). Nothing to do.
- `extension-carried`: a module of the game's own, now in `Extensions.<PackId>` (as is `extension-override`, the game's edited version of a shipped one). Check each in Play as its `fix` says: wait as section 7's Play, Server snippet does (for General UI Kit's `CustomHandlers`, which the kit loads on the client, section 7's Play, Client snippet, which waits for `RBLXE_ClientBooted`), then read `get_console_output` and confirm that no warning names the module (its `fix` gives its pack's words, such as `failed to load`, `errored while starting`, `did not start`, `is disabled`, or the kit's `errored` and `has not returned`). A Notifications System trigger must also be in the line `[NotificationsSystem] ready. Triggers on: ...`: as `<Name> (game)` before `; off:` when it started, or after `off:` when it is switched off (then it was not loaded: tell the user it is switched off, as the game had it, so Play does not prove it). A trigger of the game's own (`extension-carried`) is switched off by its `Enabled` attribute set to false; the game's version of a shipped trigger (`extension-override`) by `Triggers.<Name> = false` in its config module, never by its `Enabled` attribute, and `<Name> (pack instead of game)` in that line means it did not start and the pack's own runs instead. A reward module (a `Custom` reward's `Target`) runs only when a player gets that reward: tell the user it is not proven in Play unless that reward is granted. In Codes System and Time Rewards System, a reward module whose `Prepare` fails or returns false disables every reward that names it with no line in the Output that names the module (a day it disables stops the daily cycle, and only section 7's daily-cycle line says so), so a clean Output does not prove it either: redeem a code that names it (a disabled one answers `CodeDisabled`) or claim a gift or day that names it (a disabled one never becomes claimable). Its `fix` gives that clause only when the module has a `Prepare`; for one without, it says the pack never disables it without a warning that names it, so a clean Output proves it loaded when a reward names it.
- `retired-kept` and `content-kept`: the game's interface and content, kept as players had them: what the game changed stays live, what it removed stays removed (no 3.0 default in its place). A `content-kept` that says unverified names content Tools cannot compare (a Part, a mesh, a SoundGroup): it stays as 2.x had it whether or not the game changed it, so never tell the user the game changed it. Nothing to do unless the user says so, or a `content-kept` names a reference it could not point (below). Otherwise the quoted `report` names them: restate none.
- `foreign-copy-live`, `interface-migrated` and `migrate-validated`: see menus and HUD buttons, below.
- `migration-note`: a 2.x setting not carried as it stood: the user decides whether the game wants otherwise. Show the user its message (`report` quotes its first sentence; the rest is in the answer if the user asks); its `fix` says what to change if the game wants otherwise.
- `reads-2x-settings`: the search of every script of the place the run made itself, for every pack it migrated: each script in `Workspace`, `Lighting`, `MaterialService`, `ReplicatedFirst`, `ReplicatedStorage`, `ServerScriptService`, `ServerStorage`, `StarterGui`, `StarterPack`, `StarterPlayer`, `Teams`, `SoundService`, `Chat` and `TextChatService`, read once, but none in `ServerStorage.RBLXEssentials`; in the Kernel's folders, the two `Config` folders that hold the config modules and the packs' own folders (each official pack's, and each pack the run migrated) it looks only for a `<PackId>.Settings` or `Packs.<PackId>.Config` path, never for a setting's name, except in the game's content kept live inside the packs' own folders, `content-kept` (and what the game put next to it), which it reads like any other script. Each names a script of the game's that still reads a pack's 2.x Settings or Config, uses a 2.x setting by name, or finds a pack's interface at its 2.x path. It is an action, not information: show the user the script and what it reads, and with their agreement change it as its `fix` says (options are read once, at server start, from the modules `T.config.where("<PackId>")` names; pieces by their pack's attributes and tags, as `T.describe({ pack = "<PackId>" })` shows), then check it in Play. A note resting on the same search (Sound System's live `SFXEnabled`) is a `migration-action` whose `fix` names each script found, or a `migration-note` whose `fix` says no script of the game uses those names, and its line in `report` ends with what the search found: there is nothing left to search by hand.

When you tell the user what the migration did, name what the answer names (each `content-kept`, `extension-carried` and `retired-kept` target, and each `migration-note` and `migration-action` by its line in `report`, which you quote whole; the answer's issue holds its whole message), never this guide's examples; the migration backup holds the place's RBLX Essentials folders, not the whole place.

A migration never touches player data.

What stays the game's, and what stops a migration:
- Content the game changed (a pet catalog, boards, sounds, skins) stays live and marked (`content-kept`), as does what the game put next to it (a board next to `Boards`, a sound next to `SFX`); later installs and updates keep it (`game-content-kept`). It is the same instance, so a reference to it holds. A reference inside it (an `ObjectValue`, a Sound's `SoundGroup`, a weld, a constraint) to a 2.x default the migration replaced points at the 3.0 instance at the same path; one that cannot has its own `content-kept` warning: point it where the game needs, as its `fix` says.
- A menu or HUD button players saw stays. The kit has a piece for each of its 14 menus (section 11): an unedited copy drops, an edited one is forked (`GeneralUIKit_<piece>`) and, once the owner of a stand-in is installed, becomes the game's fork of its piece (`standin-fork-adopted`: check in Play; the game already having its own fork of the owner's piece gives `standin-fork-kept`, a fork that fails its contract `standin-fork-refused`: ask the user). A 3.0.0 game's kept copy is removed when unedited (`kept-copy-superseded`), forked when edited (`kept-copy-forked`), hidden when the game already forks that piece (`kept-copy-hidden`: ask the user); its own menu on a kit slot outside `Interface/Custom` is named (`own-menu-shadowed`: ask the user); a copy kept in `Interface/Custom` by an earlier migration meets its owner as `foreign-copy-forked` (edited) or `foreign-copy-superseded` (unedited). A copy the game has since moved to another slot (`PanelId`, `TargetPanel`) or untagged stays the game's menu (`foreign-copy-own`, named once, never forked or replaced). When that pack migrates in the same run, it decides General UI Kit's 2.x copy of its menu or button, and each report needs nothing: the copy players saw is `foreign-copy-live` (kept or forked), an unedited copy that lost its slot goes with the kit's 2.x folder (`foreign-copy-dropped`), an edited copy players never saw stays in the migration backup (`foreign-copy-edited`; `custom-code-carried` names a script of the game's in it). `interface-migrated` sums up each pack's interface: nothing to do (`T.describe({ pack = "<PackId>" })` shows the copy the Kernel uses for each piece). `migrate-validated` gives the counts of the run's final `T.validate()`: with warnings or errors, run `T.validate()` and do what each `fix` says. A HUD button, widget or layer the game deleted in 2.x (SoundSystem's music player, NotificationsSystem's layer) is kept as a hidden fork (`deleted-kept-hidden`), and a menu whose button is hidden that way is named too. A widget its pack shows on its own (EasyPetSystem's `HatchReveal` and `HatchRevealTriple` on each hatch, GeneralUIKit's tooltip frames, section 11) still appears then, from that fork, whatever its `Visible`, as its warning says: if the game does not want that either, ask the user. `T.content.deactivate("<PieceId>")` drops a hidden fork the game does not want (the pack's own piece is used again). A menu the game deleted while its button stayed stops the migration (`migrate-deleted-piece`, below).
- A script that makes the identity of saved data (DatastorePurchaseSystem's `Config`, `DataStoreManager`, `DefaultData`, `StudioSafeId`, which builds each player's DataStore key; EasyLeaderstatsSystem's `Config`, `GlobalBoards`) edited or missing: `migrate-identity-modified`, never forced. Its message says which:
  - **Edited**, even where the edit does not touch the store: the message names the lines that differ. Put the published script back only when the store or profile it then uses is the one the game should keep, and only once the user, shown those lines, agrees. A line that is not a value the 2.x Settings hold is the game's own code, which putting the script back deletes: keep a copy of the game's version outside RBLX Essentials (a ModuleScript in `ServerStorage`, outside `ServerStorage.RBLXEssentials`) unless the user says they do not need it. Move a value the 2.x Settings can hold (a store version, a default) into them first; for a store name or key written only in code, ask the user, and neither put the script back nor migrate yet. Follow the rest of its `fix`.
  - **Missing**, or something that is not a script has its name: Tools cannot tell which store or profile the game used. Before migrating, move what has its name (with what it holds) where the game needs it, and put the script back at the path the message names: the game's own version when it had one (Studio's undo, or a copy the user kept), else the published one; ask the user when you do not know which one the game ran. A version that differs from every published one is then refused as an edited one.
- The 2.x Settings deleted after the first run: `migrate-v2-settings-gone`, never forced: put them back from the stored copy the message names. Never delete a 2.x `Settings` folder before `T.migrate()` reports `done`.
- A pinned option (section 11) is written as 2.x ran it and never changed; a module that says otherwise stops the migration (`migrate-config-pinned`), never forced.
- A module carried to `Extensions.<PackId>` (`extension-carried` or `extension-override`) that reads `ctx.settings` or the 2.x `Config` (`reads-ctx-settings`), or reaches the pack's modules through `script.Parent` (`reads-script-parent`), no longer finds them: after the migration, change it as the message's `fix` says and check it in Play. Two modules of the game's own with one name in a folder the migration carries stop it (`migrate-keep-duplicate`): keep one.
- A run that stopped half way: `migrate-incomplete` or `migrate-unfinished`. Check the place with `T.describe()` and repair what the message names; only when the user confirms the repair, pass that run's `"migrate-unfinished <backup> <state>"` entry in `force` (a refused run lists it again in `data.force`, with `migrate-repair-pending`).

A value 2.x ran that 3.0 cannot take stops it: `migrate-config-invalid` (the Schema refuses it, such as a `Stat` Target made only of dots) or `migrate-config-unwritable` (not writable as Luau, such as an infinite `Amount`). Its `target` is the 2.x slot, such as `Settings.Codes.Code9.Target`: ask the user how to change that slot in the 2.x Settings, then run `T.migrate()` again.

Four refusals can be lifted with `force`, and only with the user's decision:
- `migrate-custom-modified`: the game edited a module the pack ships (GrantPet, the redemption rules in `Codes`, the server module...). The message names each module and the lines that differ. With force, the 3.0 version replaces it and the edited copy, as that run found it, stays in the protected migration backup, whose path its `custom-module-replaced` warning names.
- `migrate-custom-unknown`: the game put a script of its own in the pack's folders, outside the folder the migration carries (`CustomRewards` for Codes System), where the update would drop it; the message names each. With force, it is not carried into 3.0, and its copy stays in the protected migration backup, whose path its `custom-module-dropped` warning names.
- `migrate-config-differs`: a config module exists without the migrated values. With force, the module is kept as it is (`config-kept`). To merge instead, copy the migrated values from the answer's `texts["<PackId>.<side>"]` into the module, with that text's first line (`--rblxe-migrated v1 ...`, which records what was converted, so a later run can tell a value the game removed since).
- `migrate-deleted-piece`: the game deleted a menu in 2.x (players saw none) while its button stayed; in 3.0 that button would open the pack's default menu. The message names the menu and why it was missing. With force, the default comes back (`no-live-copy` names it). If the user does not want it back, the game deletes the button too before the migration (a deleted button is kept hidden: nothing opens the menu); if the game needs that button for something else, it stays on 2.x for now.

When every refusal of a run is one of these, `data.force` lists one entry per refusal (each refusal also carries its own, in its `force` field): `"<code> <target> <SourceHash>"`, such as `"migrate-custom-modified ServerScriptService.RBLXEssentials.Packs.CodesSystem.CustomRewards.GrantPet 1a2b3c4d"` (the hash fingerprints that script's text). Show the user every refusal; if they agree to each, run `T.migrate({ force = { "<entry>", "<entry>" } })` in place of `T.migrate()` in the snippet, with exactly that run's `data.force` (the answer's `force`). Force lifts nothing unless the run's refusals are exactly those entries: if anything changed since the user decided (a refusal new or gone, a script edited again), it refuses with `migrate-force-not-applied`, changes nothing and gives the new list in `data.force`: show the user the new refusals. A code alone (`{ force = { "migrate-custom-modified" } }`) or `{ force = true }` lifts nothing here.

## 9. Backups

```lua
-- Edit: the backups (Tools never deletes one on its own)
local T = require(game.ServerStorage.RBLXEssentials.Tools)
return game:GetService("HttpService"):JSONEncode(T.backups.list().data.reasons)
```

A protected backup holds the game's own work or something Tools could not verify. `T.backups.delete(id)` deletes the one named `id`; a protected one needs `{ force = id }` (rule 7).

## 10. Issue codes you will meet

- `not-in-edit`: the game is running; stop it.
- `container-not-found`: no container at that reference; call `T.describe` for the right one.
- From the website (section 4): `website-unavailable`: no RBLX Essentials Plugin 3.1 answers in this Studio. `website-sign-in`: the plugin is not signed in. `website-not-owned`: the account does not own the pack. `website-failed`: the website could not give the pack. `website-build-failed`: the download could not be built; nothing changed.
- `missing-dependency`, `already-installed`, `kernel-major-upgrade`, `kernel-major-mismatch`: section 4 (and sections 7 and 8 for a 2.x pack).
- `product-code-unverified`: a Kernel or pack from a pre-3.0 container has no hashes, so Tools cannot check its code: information in `T.describe`, `T.validate`, a warning when an install or update replaces it (kept in `Backups`).
- `config-pending`: config modules to create (section 4).
- `config-invalid`: the configuration breaks a rule; the message names the option. `codes-example-live`, `demo-prop-live`: section 7. `robux-egg-no-product`: section 11 (Easy Pet System). `codes-custom-unavailable`: a Custom code's pack (GrantPet: Easy Pet System) is missing: the user decides.
- `override-rejected`: an `Interface` piece does not satisfy its contract; the pack's default is used.
- `migration-available`, `migration-action`, `migration-note` and every other code section 8 names: section 8.
- Installing, updating and removing: `container-invalid`, `container-content-invalid`, `container-not-archivable`, `hashes-invalid`, `older-version`, `pack-duplicate`, `reserved-pack-id`, `kernel-missing`, `kernel-not-installed`, `kernel-too-old`, `not-installed`, `dependents-installed`, `packs-installed`, `missing-root`, `install-failed`, `uninstall-failed`, `remove-kernel-failed`, `restore-failed`: the operation stopped and changed nothing (or undid itself); do what the `fix` says. `container-foreign-pack` and `container-touches-game-zone`: not an RBLX Essentials export; ask the user to get the pack's `.rbxm` again.
- `product-code-modified`: the game changed the pack's own code: an update replaces it only with the user's `force`, keeping the game's version in `Backups`; first move the game's change to its own zones (configuration, a fork, `Extensions`).
- `product-content-modified`, `default-piece-modified`: the game changed the pack's own content, or edited a default piece in place. An update replaces it without asking (the game's content in it stays live), keeping the edited copy in the protected backup the message names; to keep such an edit, fork the piece or move the content to game-owned instances first.
- `default-piece-invalid`: the pack's own piece is broken; update the pack.
- `product-content-unverified`: after a migration it needs nothing (section 8). On a later update it names content Tools could not compare, which the update replaced, kept in the protected backup the message names: ask the user whether the game changed it, and copy what it still needs from that backup into game-owned instances.
- `update-pinned-default`, `install-pinned-unstated`: a pinned option the config module leaves out would take a default the game may not have run: write the value the game runs into the module, then run it again (`T.update` refuses every unstated pinned option when it cannot read the installed Schema). When you cannot tell that value (that case, or a pack installed again over config modules `T.uninstall` kept), ask the user. `force` never lifts them.
- `game-content-kept`, `game-content-moved`, `game-content-replaced`: what an install or update did with the game's content (scripts included); the message says where it is.
- `config-write-failed`, `config-side-invalid`, `setting-dropped`, `schema-invalid`: a config module Tools could not write, a `T.config.create` `side` other than `"shared"` or `"server"`, a 2.x setting the pack no longer takes, a broken `Schema` (get the pack's `.rbxm` again). `orphan-config`, `orphan-extension`: a config module or `Extensions` folder whose pack is not installed: install the pack again, or leave them (they hold the game's values and code; move code the game needs into its own scripts). Never delete either without asking the user, whatever the `fix` says.
- Themes and pieces (section 6): `content-invalid`, `content-has-script`, `content-not-installed`, `not-a-theme`, `theme-missing`, `piece-missing`, `piece-not-in-theme`, `piece-outside-root`, `piece-duplicate`, `piece-contract`, `piece-identity`, `piece-undeclared`, `piece-no-system`, `pieces-invalid`, `pieces-plan`, `duplicate-panel-id`, `no-default`, `fork-failed`, `activate-failed`, `deactivate-failed`, `reconcile-failed`: the message names the piece and the `fix` what to change. `override-modified`, `override-unverified`, `override-migrated`: a piece in `Interface` the game edited or placed by hand, that Tools cannot compare, or the 2.x copy the migration kept: replaced or removed only with the user's `force` (its entry in `data.force`), after a copy goes to `Backups`.
- Backups (section 9): `backups-large`, `prune-invalid`, `backup-not-found`, `backup-needed`, `backup-protected`.
- Migration, besides section 8's: `migrate-v2-refused`, `migrate-stopped`, `migrate-code-unplanned`, `migrate-not-archivable`, `migrate-pack-duplicate`, `migrate-content-duplicate`, `migrate-config-module-invalid`, `migrate-fork-refused`, `extension-exists`, `keep-child-replaced`: the message says what stopped or what was kept, and the `fix` what to do. `config-zone-invalid`, `interface-zone-invalid`, `extensions-zone-invalid`: a zone the migration writes into (`Config`, `Interface`, `Interface/Custom`, `Extensions`) is not a Folder; ask the user before renaming what the game put there, then run it again. `migrate-convert-failed`, `migrate-no-converter`, `migrate-schema-invalid`, `migrate-pieces-invalid`, `migrate-sources-invalid`, `migrate-content-invalid`, `migrate-interface-invalid`, `migrate-keep-invalid`, `interface-published-stale` and `content-published-stale` are about the pack's 3.0 container: ask the user to get that `.rbxm` again.
- Everything else Tools reports is information (`info`): it says what happened and needs nothing.
- `internal-error`: Tools itself stopped. Show the user its message and do only what its `fix` says; do not retry blindly. After `T.migrate()`, check `T.describe().data.migration` first: a migration whose `state` is `done` is finished, so never run `T.migrate()` again.
- `follow-up-failed`: the operation is done, but one step after it failed; do only what its `fix` says. After `T.migrate()` it names the search of the game's scripts, or the copy of `report` it keeps on the migration backup: the migration is done, and the answer's `report` holds its lines.

## 11. The packs

Section 5 says how to set options (`T.config.reference("<PackId>")` lists them). A **pinned** option names saved data or something players paid for, or is a list whose positions the interface or saved progress use: a migration writes the value 2.x ran and never changes it (`force` cannot make it), and an install or update refuses to move it to a new default the config module does not state. In a live game, never change a value players' data already uses: a store version, the `StatPath` of a board that has ranks, a gamepass or product id players bought, the order of Time Rewards' `Days`. These keep that data: a new `DefaultData` key or board, a gamepass id set where it was `0`, and `Enabled = false` on an item, which keeps its place. The game's own modules for a pack live in `Extensions.<PackId>`, which no update touches.

### DatastorePurchaseSystem

Player data, purchases and gamepasses; every pack that saves data needs it. Its currencies are `Stats.Cash` and `Stats.Diamonds`, the second currency every pack grants (section 5: gems and premium currency are `Diamonds`). Options (shared): `DataStoreVersion` (pinned, `"v1"`: the live store is `PlayerData_<this>`), `StudioDataStoreVersion` (pinned, `"v4"`: keep it different from `DataStoreVersion` by more than spaces or letter case, or Studio uses live profiles), `DefaultData` (pinned: a new profile's starting data), `AutoSaveInterval` (`90` seconds) and `FreeMode` (`false`; only in Studio). No pieces. An edited or missing 2.x identity script stops a migration (section 8). A game's profile migration goes in `ServerScriptService.RBLXEssentials.Extensions.DatastorePurchaseSystem.Migration`. To test new players, never wipe profiles (rule 7): a saved profile gets each `DefaultData` key it lacks when it loads (one it has keeps its value), so read `PlayerData` in Play (section 7). Only the user wipes Studio profiles, with the plugin's Wipe Studio Data button. Security: grant developer products only in `ProcessReceipt`, through this pack's `registerHandler` or `registerProfileHandler`, never in `PromptProductPurchaseFinished`, which a client can fake; check gamepasses with `HasPass`.

### CodesSystem

Codes players redeem. Options: `Codes` (server only) and `Messages` (shared), section 5. Installs state live example codes, to replace or remove before publishing (never in a game moved from 2.x): `WELCOME2027`, and `PETDAY2027` (a `GrantPet` code) only when Easy Pet System is installed before Codes System. They are in the server config module: an install from the plugin writes it; `T.install` from `execute_luau` returns it as a pending `multi_edit` (section 4), and until that is applied the game has no codes. Pieces: `CodesSystem.CodesPanel` and `CodesSystem.CodesButton`. The game's reward modules go in `ServerScriptService.RBLXEssentials.Extensions.CodesSystem`.

### EasyLeaderstatsSystem

The player list and global boards. Options (shared): `Leaderstats` (pinned: the player-list stats, `{ DataKey, Name, Format, Prefix, Suffix }` each, in order; write the whole list) and `Leaderboards` (`RefreshInterval` 60, `MaxEntries` 50, and `Boards`, pinned: `{ Id, StatPath, Enabled, ValueFormat, Title, Header }` each). A board's store is `GlobalLeaderboard_` plus its `StatPath` with each `.` as `_`: never change the `StatPath` of a board that has ranks. No pieces: a board is a Model in the workspace tagged `ELS_Leaderboard`, found by its `ELS_LeaderboardId` attribute (the entry's `Id`). To add one, clone a design from `ServerScriptService.RBLXEssentials.Packs.EasyLeaderstatsSystem.Assets.Templates` into `Workspace.RBLXEssentials.EasyLeaderstatsSystem.Boards` (where the plugin lists them), tag it `ELS_Leaderboard`, give it a new `ELS_LeaderboardId`, and add its entry to `Leaderboards.Boards` (the whole list, written once). A stat must exist in DatastorePurchaseSystem's `DefaultData`. An edited or missing 2.x `Config` or `GlobalBoards` stops a migration (section 8).

### EasyPetSystem

Pets, eggs and hatching. Options (shared): `Gamepasses` (`TripleHatch`, `AutoHatch`, `ExtraPetSpace`, pinned gamepass ids, `0` = off), `BaseCash` (500), `RareAnnounce` (`MinOrder`, `Message`) and `PetVisuals` (`DefaultMode`, `Modes`). The mode pets start in (`Ground` walks, `Orbit` floats around the player) is `PetData.FollowMode` (a StringValue in the pet catalog below); `PetVisuals.DefaultMode` is only the fallback for a player whose mode names none of `Modes`, so changing it alone changes nothing in Play. The pet catalog, `ReplicatedStorage.RBLXEssentials.Packs.EasyPetSystem.Assets.PetData` (rarities, pets, eggs, prices, Robux product ids, limits), is the game's content, not configuration: edit it in place; updates keep a changed one. Its Robux egg, `Tier6Egg`, sells RBLX Essentials' demo developer product until the game sets its own id in `PetData.Eggs.Tier6Egg.productId` (a NumberValue): only the server's Output names it (section 7). Before publishing, the user picks one: their own developer product id there, or no Robux egg. No Robux egg means deleting both its catalog entry `PetData.Eggs.Tier6Egg` (the server's placeholder line and every Robux egg check read the catalog, not the world) and its stand `Workspace.RBLXEssentials.EasyPetSystem.Eggs.Tier6Egg` (section 12), in two `execute_luau` calls; updates keep the edited catalog. Its four Dominus pets stay in `PetData.Pets`, but with no egg the Index no longer lists them and they cannot be merged (`no_next_tier`): in a live game whose players may own them, set a real id instead of deleting the entry. 0 or a negative number is not an off switch: the egg still shows and its Open button only says "This egg isn't for sale yet" (before 3.0.3 it threw an error), and `T.validate` warns `robux-egg-no-product`. Pieces: `EasyPetSystem.PetsPanel`, `EasyPetSystem.PetsButton`, `EasyPetSystem.HatchReveal` and `EasyPetSystem.HatchRevealTriple`. FreeMode comes from DatastorePurchaseSystem: with it on, the server's Output says `[EasyPetSystem] FreeMode is on in ReplicatedStorage.RBLXEssentials.Config.DatastorePurchaseSystem`, which holds only in Studio; a live server ignores FreeMode, checks real ownership and warns at start until `FreeMode` is `false`. The install puts a demo pad in the world, `Workspace.RBLXEssentials.EasyPetSystem.AddCashPart` ("Add Cash"): its prompt gives `BaseCash` Cash to anyone, on every press, in Studio only (before 3.0.2 also on a live server: `demo-prop-live` warns). List it to the user as an action before publishing: delete the part (the pack waits for a part tagged `AddCashPart` and does nothing without one), unless the game wants the pad. The egg stands are demo placement too (section 12).

### GeneralUIKit

All 14 menus (Shop, Settings, Teleport, Changelogs, Pets, Codes, DailyRewards, Rewards, Spin, Rebirth, Upgrades, Trade, TradeInventory, TradeRequest), 11 HUD buttons, the invite button, the effects bar and the tooltips: a game with only this kit has every menu. Needs Kernel 3.0.1 (`kernel-too-old`). No options. Pieces: `GeneralUIKit.ShopPanel`, `GeneralUIKit.ShopButton`, `GeneralUIKit.SettingsPanel`, `GeneralUIKit.SettingsButton`, `GeneralUIKit.TeleportPanel`, `GeneralUIKit.TeleportButton`, `GeneralUIKit.ChangelogsPanel`, `GeneralUIKit.ChangelogsButton`, `GeneralUIKit.UpgradesPanel`, `GeneralUIKit.UpgradesButton`, `GeneralUIKit.RebirthPanel`, `GeneralUIKit.RebirthButton`, `GeneralUIKit.TradePanel`, `GeneralUIKit.TradeButton`, `GeneralUIKit.CodesPanel`, `GeneralUIKit.CodesButton`, `GeneralUIKit.SpinPanel`, `GeneralUIKit.SpinButton`, `GeneralUIKit.RewardsPanel`, `GeneralUIKit.RewardsButton`, `GeneralUIKit.DailyRewardsPanel`, `GeneralUIKit.DailyRewardsButton`, `GeneralUIKit.PetsPanel`, `GeneralUIKit.PetsButton`, `GeneralUIKit.TradeInventoryPanel`, `GeneralUIKit.TradeRequestPanel`, `GeneralUIKit.InviteFriends`, `GeneralUIKit.Effects`, `GeneralUIKit.ButtonDetails` and `GeneralUIKit.EffectDetails`. Ten are demo stand-ins (`standsInFor`: Codes, Spin, Rewards, DailyRewards, Pets, menu and button): their pack's piece replaces them when installed, and a fork (`T.content.fork("GeneralUIKit.CodesPanel")`) becomes the game's fork of the pack's piece. Rebirth, Upgrades and Trade are templates with no pack. In the Shop menu only the three Easy Pet System passes (`BuyX3Eggs`, `BuyAutoHatch`, `BuyExtraPetSpace`, sold through its `Gamepasses` option) do something; X2 Luck, X2 Cash, X2 Speed, X2 Time, X2 Rebirth, Starter Pack and the Ingot packs are templates with no pack behind them, and every price on a Shop item is fixed text. Tell the user which items sell nothing; to hide them, fork `GeneralUIKit.ShopPanel` and hide them on the fork. The game's button actions go in `ReplicatedStorage.RBLXEssentials.Extensions.GeneralUIKit.CustomHandlers` (a module returning `{ [Action] = function(button) ... end }`): `ClaimCode` (Codes), `Spin`, `Buy1Spin`, `Buy3Spin`, `Buy10Spin` (Spin; quiet stubs until wired), and any `Action` the game gives a button of Rebirth, Upgrades or Trade. To hide an element, fork its piece and set `Visible = false` on the fork (for a menu, on the fork of the button that opens it); the tooltip frames are the exception: the kit still shows `EffectDetails` over an effect icon and gives `ButtonDetails`' look to every HUD button's tooltip, whatever the fork's `Visible`. Theme: Studded UI (`StuddedUI`, section 6).

### NotificationsSystem

Toasts, banners and chat lines. Options (shared): `Announcements` (`IntervalMinutes`, `Surface`, `Shuffle`, `Messages`), `Triggers` (on or off: `Announcements`, `FriendJoined`, `JoinLeave`, off by default, and `Purchases`) and the texts of `FriendJoined`, `JoinLeave` and `Purchases`. Piece: `NotificationsSystem.NotificationLayer` (a layer: fork it to change the card templates). The game's triggers go in `ServerScriptService.RBLXEssentials.Extensions.NotificationsSystem` (`Start(ctx, trove)`; they read `ctx.config`, never `ctx.settings`).

### RouletteSystem

The spin wheel, 2 to 12 slots: one per `Wheel` label. Options: shared `Spins` (`StartingSpins`, `MaxSpins`, `FreeSpinCooldown`), `DevProducts` (`OneSpin`, `ThreeSpins`, `TenSpins`: pinned product ids, the game's own replacing the demo ones) and `Wheel`; server only `Rewards` (`{ Slot, Type, Target?, Amount, Weight }`; left out, the shipped wheel). Pieces: `RouletteSystem.SpinPanel` and `RouletteSystem.SpinButton`. A player's spins are `PlayerData.Roulette.Spins` (beside `LastSpinTime`, the free-spin clock). The game's reward modules go in `ServerScriptService.RBLXEssentials.Extensions.RouletteSystem`; other packs grant spins through its `grantSpins`.

### SoundSystem

Music and sound effects. Options (shared): `SFXEnabled`, `MusicPlayer` (`Enabled`) and `Volumes` (`SFX`, `Music`, 0 to 10; unset keeps each group's own). While the game runs, the `SFXEnabled` attribute of `ReplicatedStorage.RBLXEssentials.Packs.SoundSystem`, or the pack's `SetSFXEnabled` and `IsSFXEnabled`, switch sound effects. Piece: `SoundSystem.MusicPlayer` (a layer; for music with no player on screen, fork it and set `Visible = false` on the fork's frame). The library `SoundService.RBLXEssentials.SoundSystem.SFX` and `Music`, the Demo and the skins are the game's content. The Demo includes five test pads with prompts in `Workspace.RBLXEssentials.SoundSystem.Demo` that play sounds, also live (`demo-prop-live`): list them to the user before publishing, to delete or keep (section 12).

### TimeRewardsSystem

Session gifts and daily rewards. Options (shared): `SessionGifts` (pinned: gift N is the Rewards panel's frame `GiftN`) and `DailyRewards` (`ResetMode`, `CooldownHours`, and `Days`, pinned: day N is the tile `DayN`). Session gifts are not saved: a player who rejoins starts them again. Each player's saved streak is a position in `Days`: in a live game never add, remove or reorder days. To stop a gift or a day, set `Enabled = false` on it: it keeps its place and is never granted (a day that is off stops the daily cycle there). A `Spins` day needs RouletteSystem and a `GrantPet` day needs EasyPetSystem: without that pack the day grants its `Fallback` (a Cash, Diamonds or Stat reward) or, with none, is disabled and the daily cycle stops there for every player who reaches it (a migrated game keeps its 2.x days). Default Day 3: a spin with a 300 Cash `Fallback`. Tell the user (section 7); change `Days` only once the user decides, never on your own. Pieces: `TimeRewardsSystem.RewardsPanel`, `TimeRewardsSystem.RewardsButton`, `TimeRewardsSystem.DailyRewardsPanel` and `TimeRewardsSystem.DailyRewardsButton`. The game's reward modules go in `ServerScriptService.RBLXEssentials.Extensions.TimeRewardsSystem`.

## 12. Objects in the world are demo placement

Three packs put objects in `Workspace.RBLXEssentials.<PackId>` when they install. They stand where the demo needs them, not where the user's game does: games use the packs' default values, but no game leaves these models where the install put them. Help the user place them in their map, restyle or replace the models, and remove the test tools. Each pack finds its objects by tag anywhere in `Workspace`, not by path.

| Object | What it is | What the pack's code needs |
|---|---|---|
| `EasyPetSystem.Eggs`: `Tier1Egg` to `Tier6Egg` | A real feature: egg stands where players open each egg. Only place them. | A Model tagged `Egg` with the attribute `EggId` (a key of `PetData.Eggs`); in it a BasePart named `Gui` holding the BillboardGui tagged `EggBoard` (`ObtainablePet` with `Container`, `EggName` and the `Open1`, `Open3` and `Auto` buttons) and `PriceAttachment.PriceBillboardGui.Frame.PriceText`. The board shows within `PetData.ProximityDistance` studs of `Gui`. A child Model named `Egg` floats; a part tagged `EPS_RGBLoop` cycles its colour (`Tier6Egg`); `Base` and `Hitbox` are decoration. |
| `EasyPetSystem.AddCashPart` | A test tool: `BaseCash` Cash on every press, in Studio only (section 11). Remove it before publishing. | A part tagged `AddCashPart` with a ProximityPrompt in it. |
| `EasyLeaderstatsSystem.Boards`: `Cash Leaderboard`, `Wins Leaderboard` | A real feature: global boards with the top three on podiums. Only place them. | A Model tagged `ELS_Leaderboard` whose attribute `ELS_LeaderboardId` is the `Id` of its `Leaderboards.Boards` entry (no `Id`: the Model named `<last key of StatPath> Leaderboard`); descendants whose attribute `ELS_Role` is `List` (with its `Template` row), `TitleLabel` and `HeaderValue`. Optional: `Timer.GUI.Label` (the countdown) and the rigs `Dummy1` to `Dummy3`. |
| `SoundSystem.Demo`: five pads | Test tools: they play a sound when pressed, also live. Remove them before publishing unless the game wants them. | Parts tagged `SoundSystem_Demo` with a ProximityPrompt and the attributes `SFX_Action`, `SFX_Sound` and `SFX_Range`. |

- **Moving:** move them anywhere (`PivotTo`), as descendants of `Workspace`. Keep them inside `Workspace.RBLXEssentials.<PackId>` (the folder does not fix their position): there, `T.update` keeps the game's copy live (`game-content-kept`) and adds no default beside it. Moved out of that folder they still work, but an update can place the pack's default in the folder again, and `T.uninstall` leaves them behind.
- **Restyling or replacing:** keep what the table names; change the rest freely. For another egg stand, copy `ReplicatedStorage.RBLXEssentials.Packs.EasyPetSystem.Assets.Templates.EggTemplate` into the world and set its `EggId`.
- **Deleting is safe:** nothing errors and `T.validate` reports nothing missing (only `demo-prop-live` while a test tool is there). A deleted egg stand: that egg can no longer be opened in the world. A deleted board: it is not drawn, scores still save, and a board added later is used. After an update, an object the game deleted stays deleted when the report says `game-content-kept` with "keeps it removed"; otherwise the pack's default is back in its folder: tell the user.

The installed sources and manifests are the final reference for your version. This guide holds no credentials, private paths or paid source code.
