> For the complete documentation index, see [llms.txt](https://atiysus-organization.gitbook.io/aty-scripts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://atiysus-organization.gitbook.io/aty-scripts/shell-creator.md).

# Shell Creator

## aty\_shellcreator

In-game grid interior builder: design shell interiors on a 2 m grid from a modular kit (floors, walls, openings, stairs, ceilings, decor, lights, anchor points), preview them by walking through, publish them, and let other resources such as `aty_housing` use them as interiors. The asset companion `aty_shellcreator_kit` supplies the 3D pieces the builder places.

> **Version 1.0.0.** Live-tested on QBCore. ESX and Qbox are supported through `aty_lib` but were not live-tested. Tested scope lists what was seen working in game and what was only checked with offline test harnesses.

### Info

|                     |                                                                                                                                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Resource name       | `aty_shellcreator` (companion asset: `aty_shellcreator_kit`)                                                                                                                                    |
| Version             | `1.0.0` for both resources (kit data version 7)                                                                                                                                                 |
| Frameworks          | ESX / QBCore / Standalone (detected via `aty_lib`). Live-tested on QBCore only                                                                                                                  |
| Database            | MySQL through `aty_lib`. Tables are created and migrated automatically on start, no SQL file to import                                                                                          |
| Lua version         | Lua 5.4 (`lua54 'yes'`), `fx_version 'cerulean'`, `game 'gta5'`                                                                                                                                 |
| Sides               | Client + server (shared config, grid model and locales)                                                                                                                                         |
| NUI                 | Yes — Vue 3 + Pinia, pre-built into `ui/` (shell library, editor panels, details, import and export dialogs)                                                                                    |
| Languages           | English (`en`), Turkish (`tr`), German (`de`), French (`fr`), Spanish (`es`) and Brazilian Portuguese (`pt-br`), selected with `Config.Language`; a missing key falls back to English           |
| Grid                | Cell 2.0 m, storey height 3.2 m, wall and slab thickness 0.2 m. Default shell 20 x 20 cells (40 x 40 m), at most 32 x 32 cells (64 x 64 m), up to 6 levels including basements down to level -3 |
| Third-party notices | `docs/LICENSES.md`                                                                                                                                                                              |

#### Tested scope

| Area                                                                              | Status                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Resource start on QBCore with `aty_lib`                                           | Verified live: starts without errors, database migration runs, the kit is resolved and its hash is stored, `ready` is announced                                                                                                                                                                                                                                                                                                         |
| Kit pieces in game                                                                | Verified live during kit development: own floor, ceiling and wall pieces have collision, the own stair works, door leaves fit their frames and swing. The kit's own notes record an in-game collision probe on 2026-09-30. The current kit data version 7 (67 materials) was not re-checked piece by piece in game for this page                                                                                                        |
| Console import and publish                                                        | Verified live: sample interiors were created from JSON files with the console `import` command and published as revision 1                                                                                                                                                                                                                                                                                                              |
| `aty_housing` registration                                                        | Verified live: published shells were registered by `aty_housing` as `sc_<id>` interiors and cached in its `aty_housing_built_shells` table                                                                                                                                                                                                                                                                                              |
| Opening the builder from the `aty_housing` creator                                | A defect was found live (the player was moved into an invisible editor bucket with no screen) and fixed. Re-tested in game after the fix: works                                                                                                                                                                                                                                                                                         |
| Implemented and checked with offline test harnesses, **not yet verified in game** | Per-role permissions, multi-level builds and stairs, painting, undo and redo limits, prop budget, walk-through preview, drafts and autosave, revisions, members, import and export (share code and JSON), the 16 atmosphere presets, photo and plan thumbnails, coordinate-mode shells with teleporters and vehicle entry, the stay and restart watchdogs, shell visits for admins, and entering a property on a built shell end to end |
| ESX, Qbox                                                                         | Not live-tested. Supported through `aty_lib`, but nothing more is claimed. The framework admin check only knows `qb-core` and `es_extended`; on any other framework only the ACE permission applies                                                                                                                                                                                                                                     |

### Dependencies

| Dependency                                             | Required                           | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aty_lib`                                              | Yes (declared in `fxmanifest.lua`) | Framework, database, callback, notification, job and permission bridge. Must start first. Restarting `aty_lib` restarts every dependent resource, `aty_shellcreator` included. Requires `aty_lib` 1.3.9 or newer (the version released together with this one): it adds `HasPermission` (framework admins) and `GetPlayerJob` (job rules). With an older `aty_lib` only the ACE `aty_shellcreator.admin` and housing-ability rules match |
| `aty_shellcreator_kit`                                 | Yes, for building                  | **Not** a declared dependency, so the resource starts without it, but then no kit is resolved: the builder is disabled and the console prints `no usable shell kit - the builder stays disabled` with the reason for every source tried. Players who open the manager get "No shell kit is installed. Start aty\_shellcreator\_kit." Start it before `aty_shellcreator`; it is also picked up when it starts later                       |
| `es_extended` or `qb-core`                             | One of them                        | Detected by `aty_lib`                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `oxmysql` (or the database resource `aty_lib` detects) | One of them                        | Whatever `aty_lib` detects. Live-tested with `oxmysql` only                                                                                                                                                                                                                                                                                                                                                                              |
| `aty_housing`                                          | Optional                           | Uses published shells as property interiors. Its `HasRealtorAbility` export is only asked when a permission rule uses `housingAbility`. See aty\_housing integration                                                                                                                                                                                                                                                                     |
| `screenshot-basic`                                     | Optional (not required)            | Photo thumbnails of published shells. Without it (or with `Config.Thumbnails.photo = 'off'`) no photo is taken; floor plan images do not need it. The state is checked when a thumbnail is requested, so start order does not matter                                                                                                                                                                                                     |
| `aty_clothing`                                         | Optional                           | This resource's code does not call it. Wardrobe anchors you place in a shell are handed to hosts such as `aty_housing`, which decide what a wardrobe opens                                                                                                                                                                                                                                                                               |
| `ox_target`, `qb-target` or `aty_interactions`         | Optional                           | Used through `aty_lib` for the optional target zone of coordinate-shell teleporters (`Config.Teleporter.interact`)                                                                                                                                                                                                                                                                                                                       |
| Inventory                                              | Not needed                         | `aty_shellcreator` gives and takes no items. Every inventory that `aty_lib` supports works alongside it: `aty_inventory`, `ox_inventory`, `qb-inventory`, `qs-inventory`, `ps-inventory`, `codem-inventory`, `tgiann-inventory`, `origen_inventory`, `gfx-inventory`, `esx_inventoryhud`, `ak47_inventory` and `ak47_qb_inventory`. Live testing was done on `aty_inventory` only                                                        |

### Installation

1. Copy the `aty_shellcreator` and `aty_shellcreator_kit` folders into your resources (for example `[aty]/aty_shellcreator` and `[aty]/aty_shellcreator_kit`).
2. **No SQL import.** On every start the resource creates the tables it needs (`CREATE TABLE IF NOT EXISTS`) and adds missing columns itself. It never touches your framework's tables. If the database is unreachable the migration is retried three times (5 s, then 10 s apart); after that the console prints an error and the resource stays idle until you restart it. See Database for the table list.
3. Add both resources to `server.cfg` in the order below.
4. Give your administrators the permission (see Permissions).
5. Check the job names and grades in `Config.Permissions` (default: the job `realestate`, grade 1 and 2).
6. Edit `shared/config.lua` if needed (see Configuration) and restart the resource.
7. In game, type `/shellcreator` and create your first shell.

On the first start with `aty_shellcreator_kit`, seven sample interiors are published as shells owned by the server: `Bungalow - 2 bedrooms`, `Family house - 3 bedrooms`, `Empty Studio`, `Empty 1BR Apartment`, `Empty 2BR Apartment`, `Empty 3BR House` and `Empty Loft` (`Config.SampleShells`, files in `data/samples/`). Each one is created once: a restart never duplicates it, and a sample you delete or rename is not created again. Set `Config.SampleShells.enabled = false` before the first start to skip them.

#### server.cfg

```cfg
setr game_enableDynamicDoorCreation "true"   # door leaves: FiveM refuses to create them without it

ensure oxmysql
ensure qb-core               # or es_extended
ensure aty_lib               # always before any aty_* resource
ensure screenshot-basic      # optional, photo thumbnails
ensure aty_shellcreator_kit  # the 3D kit, before the builder
ensure aty_shellcreator
ensure aty_housing           # optional, after the builder

add_ace group.admin aty_shellcreator.admin allow
```

`aty_shellcreator` waits up to 10 seconds at start for `aty_shellcreator_kit` to begin starting, and resolves the kit again whenever the kit resource starts or stops. `aty_housing` notices `aty_shellcreator` starting or stopping at any time, so its position is not critical, but the order above is the tested one.

#### Admin permission

A player is a Shell Creator admin when either check says yes, framework first:

1. **The framework's own admin permission** (`Config.AdminPermission`): qb-core permission levels in `.qb` (default `{ 'admin', 'god' }`) or ESX groups in `.esx` (default `{ 'admin', 'superadmin' }`), asked through `aty_lib`.
2. **The ACE permission** `Config.AdminAce` (default `aty_shellcreator.admin`). It is also checked when the framework says the player is not an admin:

```cfg
add_ace group.admin aty_shellcreator.admin allow
add_principal identifier.license:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx group.admin
```

On qb-core, `group.admin` is not a permission level: a player who is in `group.admin` but not in `qbcore.admin` or `qbcore.god` needs the ACE line above. The server console always counts as an admin. To use only ACE, set the lists in `Config.AdminPermission` to `{}`.

#### Kit resource

Start `aty_shellcreator_kit` before `aty_shellcreator`. The details are in Kit resource.

#### Database

All tables are created on start. Nothing here needs manual work; it is listed so you know what to include in backups.

| Table                        | Content                                                                                                                                |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `aty_shellcreator_shells`    | One row per shell: title, description, owner, visibility, status, current revision, edit lock, world placement and teleporter settings |
| `aty_shellcreator_revisions` | Published revisions of every shell (layout data)                                                                                       |
| `aty_shellcreator_drafts`    | Working copies per character, written by autosave                                                                                      |
| `aty_shellcreator_members`   | Per-shell members (editor or viewer role)                                                                                              |
| `aty_shellcreator_snippets`  | Saved clipboard snippets per character                                                                                                 |
| `aty_shellcreator_thumbs`    | Photo and floor plan thumbnails                                                                                                        |
| `aty_shellcreator_returns`   | Return points for builders and visitors, so a player is put back where they started                                                    |
| `aty_shellcreator_log`       | Audit log (opens, publishes, deletes, refused actions, unlocks, imports)                                                               |
| `aty_shellcreator_meta`      | Schema version and the kit hash used at the last boot                                                                                  |

Notes:

* The owner of a shell is the framework character id. Shells created from the console belong to the server (no owner).
* Housekeeping runs at boot and every 6 hours: stale edit locks older than `Config.Editor.lockTimeoutSeconds` are cleared, drafts older than `Config.Drafts.keepDays` are removed, and audit rows older than `Config.Log.retentionDays` are removed.

#### Files that stay open for editing

When the resource is distributed as an escrowed asset, these stay readable and editable (`upload-config.json`):

| Path                                   | Why                                                        |
| -------------------------------------- | ---------------------------------------------------------- |
| `fxmanifest.lua`, `shared/config.lua`  | Configuration                                              |
| `server/api.lua`, `client/api.lua`     | The open integration API (exports, events, hooks)          |
| `locales/*.lua` and `locales/ui/*.lua` | Translate or reword any text, including the builder panels |
| `docs/*.md`                            | Documentation                                              |

This list is the escrow edition. The open edition (a separate download of the same resource) is not protected: its Lua files are readable and editable as well.

`data/*.json` (the base decor catalogue and the starting templates in `data/templates/*.json`) are plain data files, not Lua. In the escrow edition all other Lua files are protected. The UI source code and the development tools are not part of the package; the pre-built panel is in `ui/`. For `aty_shellcreator_kit` the open files are `fxmanifest.lua`, `kit.json` and `LICENSE.md`.

### Configuration

Everything is set in `shared/config.lua`. The file is read when the resource starts: restart `aty_shellcreator` after every change. Distances are metres unless stated, and durations say their unit in the option name. Every key is read on both sides unless noted; the server re-checks everything the client sends.

#### Core

| Option                            | Type    | Default                                                                                                                                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Config.Debug`                    | boolean | `false`                                                                                                                                     | Verbose console output                                                                                                                                                                                                                                                                                                                                                                                                          |
| `Config.Language`                 | string  | `'en'`                                                                                                                                      | `'en'`, `'tr'`, `'de'`, `'fr'`, `'es'` or `'pt-br'`. Missing keys fall back to English                                                                                                                                                                                                                                                                                                                                          |
| `Config.Command`                  | string  | `'shellcreator'`                                                                                                                            | Chat command that opens the shell library. Also the name of the server console command (see Commands)                                                                                                                                                                                                                                                                                                                           |
| `Config.ExitCommand`              | string  | `'shellcreatorexit'`                                                                                                                        | Emergency command: closes the editor or preview, restores the character and releases focus                                                                                                                                                                                                                                                                                                                                      |
| `Config.AdminPermission`          | table   | `{ qb = { 'admin', 'god' }, esx = { 'admin', 'superadmin' } }`                                                                              | Framework admin permission, checked first. `{}` skips the framework check on that framework                                                                                                                                                                                                                                                                                                                                     |
| `Config.AdminAce`                 | string  | `'aty_shellcreator.admin'`                                                                                                                  | ACE permission, the last-resort admin check                                                                                                                                                                                                                                                                                                                                                                                     |
| `Config.HideOthers`               | boolean | `false`                                                                                                                                     | `true` = non-admins never see other users' shells (owners and members still see theirs)                                                                                                                                                                                                                                                                                                                                         |
| `Config.RequireDuty`              | boolean | `false`                                                                                                                                     | `true` = job criteria in `Config.Permissions` only count while the player is on duty                                                                                                                                                                                                                                                                                                                                            |
| `Config.Kit`                      | string  | `'auto'`                                                                                                                                    | `'auto'`, `'aty_shellcreator_kit'` or `'base_temp'`. See Kit resource                                                                                                                                                                                                                                                                                                                                                           |
| `Config.SampleShells`             | table   | `{ enabled = true, list = { 'bungalow_2bed', 'family_3bed', 'empty_studio', 'empty_1br', 'empty_2br', 'empty_house_3bed', 'empty_loft' } }` | Sample interiors in `data/samples/<key>.json`, published once with the own kit (same checks as the console `import`). A published sample is remembered in `aty_shellcreator_meta` (`sample:<key>`), so it is never created twice; a shell that already has the sample's title is adopted instead. `enabled = false` = never; remove a key from `list` to skip that sample. Nothing is seeded with the temporary `base_temp` kit |
| `Config.KitExtensions`            | table   | `{}`                                                                                                                                        | RESERVED, not read by any code in this version. Leave it empty                                                                                                                                                                                                                                                                                                                                                                  |
| `Config.CustomTextures`           | table   | `{ enabled = false, slots = { walls = {}, floors = {} } }`                                                                                  | RESERVED, not read by any code in this version. Leave it off                                                                                                                                                                                                                                                                                                                                                                    |
| `Config.Doors.system`             | boolean | `true`                                                                                                                                      | Register built door leaves with the game's door system                                                                                                                                                                                                                                                                                                                                                                          |
| `Config.Doors.enableDynamicDoors` | boolean | `true`                                                                                                                                      | The server sets the replicated convar `game_enableDynamicDoorCreation` to `"true"` at start when `server.cfg` does not. Without it FiveM refuses to create door leaves. Keep the `setr` line in `server.cfg` anyway                                                                                                                                                                                                             |
| `Config.Log.retentionDays`        | number  | `180`                                                                                                                                       | Audit log rows older than this are removed                                                                                                                                                                                                                                                                                                                                                                                      |

#### Permissions

`Config.Permissions` holds one rule per capability. The rule syntax and every default are described in Permissions.

#### Grid and budget

| Option                          | Type   | Default              | Description                                                             |
| ------------------------------- | ------ | -------------------- | ----------------------------------------------------------------------- |
| `Config.Grid.defaultSize`       | table  | `{ w = 20, d = 20 }` | Size of a new shell in cells (2 m each), even numbers                   |
| `Config.Grid.minSize`           | table  | `{ w = 4, d = 4 }`   | Smallest size in cells                                                  |
| `Config.Grid.maxSize`           | table  | `{ w = 32, d = 32 }` | Largest size in cells (64 x 64 m)                                       |
| `Config.Grid.defaultLevels`     | number | `1`                  | Levels of a new shell                                                   |
| `Config.Grid.maxLevels`         | number | `6`                  | Total levels including basements                                        |
| `Config.Grid.minLevel`          | number | `-3`                 | Lowest basement level index                                             |
| `Config.Budget.maxProps`        | number | `800`                | Runtime props per shell (the config comment gives the range 50 to 1500) |
| `Config.Budget.warnAt`          | number | `0.85`               | Share of the prop budget at which the editor warns                      |
| `Config.Budget.maxDecor`        | number | `400`                | Decor items per shell                                                   |
| `Config.Budget.maxDecorPerCell` | number | `8`                  | Decor items per cell                                                    |
| `Config.Budget.maxDecorPerFace` | number | `6`                  | Decor items per wall face                                               |
| `Config.Budget.maxLights`       | number | `32`                 | Lights per shell                                                        |
| `Config.Budget.maxAnchors`      | number | `24`                 | Anchor points per shell                                                 |

A shell that goes over the prop, light or decor caps cannot be published.

#### Editor

| Option                             | Type    | Default                                       | Description                                                                                                                                                                         |
| ---------------------------------- | ------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Config.Editor.bucketBase`         | number  | `400000`                                      | Routing bucket of an editing player = `bucketBase + server id`. Keep it clear of other scripts (the comment names `aty_housing` at 100000+ and 200000+, `aty_garage_v2` at 700000+) |
| `Config.Editor.origin`             | table   | `{ x = -2800.0, y = -4600.0, z = 1100.0 }`    | Where the builder works in the world. Not checked in game for map geometry: change it if the surroundings clip                                                                      |
| `Config.Editor.keepInput`          | boolean | `false`                                       | Keep game input while the editor panel has focus. Gizmo sessions always keep it                                                                                                     |
| `Config.Editor.historySize`        | number  | `200`                                         | Undo steps (20 to 1000)                                                                                                                                                             |
| `Config.Editor.historyBytes`       | number  | `4194304`                                     | Memory cap of the undo history                                                                                                                                                      |
| `Config.Editor.autosaveSeconds`    | number  | `30`                                          | Draft write interval while there are unsaved changes (15 to 300)                                                                                                                    |
| `Config.Editor.heartbeatSeconds`   | number  | `20`                                          | Session heartbeat to the server                                                                                                                                                     |
| `Config.Editor.lockTimeoutSeconds` | number  | `90`                                          | A shell lock without heartbeat expires after this long                                                                                                                              |
| `Config.Editor.opBatchMs`          | number  | `700`                                         | How long edits are batched before they go to the server                                                                                                                             |
| `Config.Editor.maxOpsPerBatch`     | number  | `64`                                          | Edits per batch                                                                                                                                                                     |
| `Config.Editor.maxBatchBytes`      | number  | `16384`                                       | Bytes per batch                                                                                                                                                                     |
| `Config.Editor.pasteMaxCells`      | number  | `256`                                         | Largest region the clipboard pastes                                                                                                                                                 |
| `Config.Editor.spawnPerFrame`      | number  | `32`                                          | Props spawned per frame while the editor builds the view                                                                                                                            |
| `Config.Editor.edgePick`           | number  | `0.6`                                         | Distance from an edge that selects it                                                                                                                                               |
| `Config.Editor.previewKeyHoldMs`   | number  | `600`                                         | How long Backspace must be held to leave the preview                                                                                                                                |
| `Config.Editor.maxSessionMinutes`  | number  | `240`                                         | Session cap in minutes; `0` = none                                                                                                                                                  |
| `Config.Editor.idleMinutes`        | number  | `20`                                          | No edits and no camera input for this long: autosave and close                                                                                                                      |
| `Config.Revisions`                 | table   | `{ keep = 20, pinnedMax = 5, noteMax = 128 }` | Revisions kept per shell, pinned revisions that survive pruning, note length                                                                                                        |
| `Config.Drafts`                    | table   | `{ keepDays = 30, maxPerCharacter = 10 }`     | Draft retention and per-character cap                                                                                                                                               |
| `Config.Snippets`                  | table   | `{ maxPerCharacter = 30, maxCells = 256 }`    | Saved snippets per character and the largest snippet                                                                                                                                |

#### Shortcuts

`Config.Shortcuts` holds the default editor shortcut per action; players can rebind them in the editor's settings. `Config.ReservedKeys` lists keys that other resources commonly map (`1` to `9`, `F1`, `F2`, `F3`, `F5`, `F6`, `F8`, `F11`, `T`, `Tab`, `B`, `Y`, `X`, `G`, `K`, `M`, `L`, `U`, `N`, `H`, `I`, `Z`, `Alt`, `CapsLock`); these are not offered for editor shortcuts. The defaults are listed under Key bindings.

#### Storage (coordinate shells)

| Option                   | Type   | Default                                    | Description                                                                                                                            |
| ------------------------ | ------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `Config.Storage.origin`  | table  | `{ x = -2000.0, y = -5200.0, z = 1000.0 }` | Origin of the slot grid where coordinate-mode shells without their own world position are placed. Not checked in game for map geometry |
| `Config.Storage.spacing` | number | `96.0`                                     | Distance between slots. Must be at least the largest shell extent (64 m) plus 32 m                                                     |
| `Config.Storage.columns` | number | `16`                                       | Slots per row; rows grow towards -Y                                                                                                    |

Changing these after coordinate shells exist moves them; the server prints an error at boot when it detects it, and also when two coordinate shells overlap.

#### Runtime (spawning shells for players)

| Option                             | Type    | Default                                              | Description                                                                                                                |
| ---------------------------------- | ------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `Config.Runtime.spawnRadius`       | number  | `40.0`                                               | Coordinate shells build when the player is this close                                                                      |
| `Config.Runtime.despawnMargin`     | number  | `20.0`                                               | Extra distance before they are removed                                                                                     |
| `Config.Runtime.lingerSeconds`     | number  | `8`                                                  | How long a coordinate shell stays built after the player has moved out of range                                            |
| `Config.Runtime.checkMs`           | number  | `1000`                                               | How often the client checks its distance to coordinate shells                                                              |
| `Config.Runtime.jumpDistance`      | number  | `50.0`                                               | A move between two checks larger than this triggers an immediate check of coordinate shells (for example after a teleport) |
| `Config.Runtime.batchPerFrame`     | number  | `20`                                                 | Props spawned per frame                                                                                                    |
| `Config.Runtime.lodDist`           | number  | `250`                                                | Draw distance of built props                                                                                               |
| `Config.Runtime.modelTimeoutMs`    | number  | `10000`                                              | Wait for models to load                                                                                                    |
| `Config.Runtime.payloadTimeoutMs`  | number  | `12000`                                              | Wait for the shell data from the server                                                                                    |
| `Config.Runtime.maxClientProps`    | number  | `2000`                                               | Props one client keeps built across shells                                                                                 |
| `Config.Runtime.cacheShells`       | number  | `12`                                                 | Shells cached on the client                                                                                                |
| `Config.Runtime.serverCacheShells` | number  | `50`                                                 | Shells cached on the server                                                                                                |
| `Config.Runtime.kvpCache`          | boolean | `true`                                               | Cache shell data in the client's key-value store                                                                           |
| `Config.Runtime.kvpMaxBytes`       | number  | `262144`                                             | Size cap of that cache                                                                                                     |
| `Config.Runtime.freezeVehicles`    | boolean | `true`                                               | Freeze vehicles this client controls inside a shell that is about to be removed, so they do not fall                       |
| `Config.Runtime.fallbackSpawn`     | table   | `{ x = -1035.71, y = -2731.87, z = 12.86, w = 0.0 }` | Where a player is put when the shell they were in is gone                                                                  |

A new published revision reaches players already inside a shell on their next entry; there is no live respawn.

#### Network and rate limits

| Option                                       | Type   | Default                                           | Description                                                         |
| -------------------------------------------- | ------ | ------------------------------------------------- | ------------------------------------------------------------------- |
| `Config.Net.latentBps`                       | number | `131072`                                          | Bandwidth of latent uploads (bytes per second)                      |
| `Config.Net.uploadChunk`                     | number | `16000`                                           | Upload chunk size                                                   |
| `Config.Net.thumbMaxBytes`                   | number | `180000`                                          | Largest photo thumbnail                                             |
| `Config.Net.planMaxBytes`                    | number | `60000`                                           | Largest plan thumbnail                                              |
| `Config.Net.importMaxBytes`                  | number | `524288`                                          | Largest import                                                      |
| `Config.Net.layoutMaxBytes`                  | number | `524288`                                          | Largest stored layout                                               |
| `Config.RateLimits.defaultMs`                | number | `300`                                             | Minimum gap between two calls of the same kind without its own rule |
| `Config.RateLimits.ops`                      | table  | `{ perSecond = 6, burst = 12, perMinute = 2400 }` | Edit batches per player                                             |
| `Config.RateLimits.query`                    | number | `2`                                               | Query rate                                                          |
| `Config.RateLimits.payloadPerMinute`         | number | `20`                                              | Shell data requests per player and minute                           |
| `Config.RateLimits.payloadPerShellPerMinute` | number | `5`                                               | Shell data requests per player, shell and minute                    |
| `Config.RateLimits.openSeconds`              | number | `3`                                               | Gap between two editor opens                                        |
| `Config.RateLimits.saveSeconds`              | number | `10`                                              | Gap between two saves                                               |
| `Config.RateLimits.importPerMinute`          | number | `3`                                               | Imports per minute                                                  |
| `Config.RateLimits.teleportSeconds`          | number | `2`                                               | Gap between two teleporter uses                                     |
| `Config.RateLimits.moveSeconds`              | number | `5`                                               | Gap between two shell moves                                         |

#### Thumbnails, atmosphere and lights

| Option                                    | Type    | Default                                                    | Description                                                                                                                                                                                                                                                                                                                         |
| ----------------------------------------- | ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Config.Thumbnails.photo`                 | string  | `'auto'`                                                   | `'auto'` takes a photo when `screenshot-basic` runs; `'off'` never does                                                                                                                                                                                                                                                             |
| `Config.Thumbnails.width`, `.height`      | number  | `480`, `270`                                               | Photo size                                                                                                                                                                                                                                                                                                                          |
| `Config.Thumbnails.quality`               | number  | `0.8`                                                      | Photo compression quality                                                                                                                                                                                                                                                                                                           |
| `Config.Thumbnails.planSize`              | number  | `512`                                                      | Floor plan image size                                                                                                                                                                                                                                                                                                               |
| `Config.Thumbnails.yaw`, `.pitch`, `.fov` | number  | `45.0`, `38.0`, `40.0`                                     | Camera angle of the photo                                                                                                                                                                                                                                                                                                           |
| `Config.Thumbnails.fill`                  | number  | `0.8`                                                      | How much of the frame the shell fills                                                                                                                                                                                                                                                                                               |
| `Config.Thumbnails.settleMs`              | number  | `600`                                                      | Wait before the photo is taken                                                                                                                                                                                                                                                                                                      |
| `Config.Atmosphere.presets`               | table   | 16 presets                                                 | Ids: `neutral`, `warm`, `cool`, `dim`, `night`, `neon`, `clinical`, `industrial`, `luxury`, `vintage`, `noir`, `sunset`, `foggy`, `bright`, `cinematic`, `dark`. Each has a label key, a timecycle modifier name, a strength and optionally an `hour` or `weather` override. Names the running game build does not know are skipped |
| `Config.Atmosphere.default`               | string  | `'neutral'`                                                | Preset of a new shell                                                                                                                                                                                                                                                                                                               |
| `Config.Atmosphere.weathers`              | table   | `{ 'EXTRASUNNY', 'CLEAR', 'CLOUDS', 'OVERCAST', 'FOGGY' }` | Weather locks a builder can choose                                                                                                                                                                                                                                                                                                  |
| `Config.Atmosphere.defaultHour`           | number  | `12`                                                       | Clock lock inside a shell                                                                                                                                                                                                                                                                                                           |
| `Config.Atmosphere.defaultWeather`        | string  | `'EXTRASUNNY'`                                             | Weather lock inside a shell                                                                                                                                                                                                                                                                                                         |
| `Config.Atmosphere.noRain`                | boolean | `true`                                                     | No rain inside a shell                                                                                                                                                                                                                                                                                                              |
| `Config.Atmosphere.pauseEvents`           | table   | `{ enter = {}, leave = {} }`                               | Client events to fire when a player enters or leaves a shell, to pause and resume a weather or time sync script, for example `{ { event = 'qb-weathersync:client:DisableSync' } }`                                                                                                                                                  |
| `Config.Lights.maxDrawn`                  | number  | `24`                                                       | Lights drawn at once on one client                                                                                                                                                                                                                                                                                                  |
| `Config.Lights.drawDistance`              | number  | `30.0`                                                     | Light draw distance                                                                                                                                                                                                                                                                                                                 |
| `Config.Lights.defaultRange`              | number  | `6.0`                                                      | Range of a new light                                                                                                                                                                                                                                                                                                                |
| `Config.Lights.defaultIntensity`          | number  | `1.5`                                                      | Intensity of a new light                                                                                                                                                                                                                                                                                                            |

#### Teleporter (coordinate shells)

| Option                                 | Type    | Default                                                                               | Description                                                                                           |
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `Config.Teleporter.enabled`            | boolean | `true`                                                                                | `false` turns every shell teleporter off                                                              |
| `Config.Teleporter.interact`           | string  | `'both'`                                                                              | `'marker'`, `'target'` or `'both'`; a shell's own teleporter setting wins                             |
| `Config.Teleporter.key`                | string  | `'E'`                                                                                 | Default key of the `aty_sc_use` key mapping                                                           |
| `Config.Teleporter.drawDistance`       | number  | `20.0`                                                                                | The ring marker shows within this distance                                                            |
| `Config.Teleporter.interactDistance`   | number  | `1.8`                                                                                 | The prompt works within this distance                                                                 |
| `Config.Teleporter.standStillMs`       | number  | `500`                                                                                 | The player must stand still this long before the prompt appears                                       |
| `Config.Teleporter.fadeMs`             | number  | `400`                                                                                 | Screen fade time                                                                                      |
| `Config.Teleporter.instanceBucketBase` | number  | `500000`                                                                              | Routing bucket of a per-player instance = base + server id                                            |
| `Config.Teleporter.marker`             | table   | `{ type = 25, size = { x = 1.1, y = 1.1, z = 1.1 }, color = { 105, 255, 236, 110 } }` | Ring marker style                                                                                     |
| `Config.Teleporter.watchdogSeconds`    | number  | `5`                                                                                   | Interval of the server check for players standing inside a restricted coordinate shell without access |

**Restricted teleporters.** A teleporter whose access is not `everyone` is restricted. Its entry point is sent only to players who may use it (admins, players with a live grant, and players who pass the access rule: jobs, ACE, members or the `canUseTeleporter` hook, cached for 30 seconds). Everyone else receives the shell with `entry = false` and `restricted = true`: no ring, no prompt, no coordinates. The list is re-sent to a player whose right changes (job, ACE, grant, membership). The exit inside a shell always works.

The server also guards the shell itself. Every `watchdogSeconds` it checks the players in the default routing bucket: a player standing inside a restricted coordinate shell without a grant is moved back to the teleporter entry by the server (the vehicle they sit in is moved with them), so a modified client cannot stay inside. Admins, players with a grant (a teleporter entry grants the shell to the driver and every passenger of the vehicle) and players in a per-player instance are not moved. Each intrusion is written to the shell's audit log (at most once a minute per player and shell).

### Commands

Every command is registered unrestricted and checked on the server; a player without the right gets a notification, nothing happens.

| Command                              | Where          | Description                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `/shellcreator`                      | Player         | Opens the shell library (manager). Needs the `open` capability                                                                                                                                                                                                                                                                             |
| `/shellcreator edit <id>`            | Player         | Opens shell `<id>` in the editor                                                                                                                                                                                                                                                                                                           |
| `/shellcreator view <id>`            | Player         | Opens shell `<id>` read-only                                                                                                                                                                                                                                                                                                               |
| `/shellcreator tp <id>`              | Player, admins | Visits shell `<id>`. `/shellcreator tp` without an id returns from the visit                                                                                                                                                                                                                                                               |
| `/shellcreatorexit`                  | Player         | Emergency exit: closes the editor or preview, restores the character, releases focus                                                                                                                                                                                                                                                       |
| `shellcreator kit`                   | Server console | Prints the active kit id, version, source, hash, grid, tint count, model count and decor count                                                                                                                                                                                                                                             |
| `shellcreator rebake [id]`           | Server console | Clears the shell data cache and rebakes the current revision of one shell or all published shells, with timings                                                                                                                                                                                                                            |
| `shellcreator validate [id]`         | Server console | Prints the publish validation result (error and warning codes) of one shell or all                                                                                                                                                                                                                                                         |
| `shellcreator unlock <id>`           | Server console | Clears the edit lock of a shell and closes the editor session holding it                                                                                                                                                                                                                                                                   |
| `shellcreator purge <id>`            | Server console | Permanently deletes a shell that is already in the deleted state                                                                                                                                                                                                                                                                           |
| `shellcreator import <file> [title]` | Server console | Creates a shell owned by the server from a JSON file inside the `aty_shellcreator` resource folder (an exported layout, or an object with `title`, `description`, `tags`, `visibility` and `layout`) and publishes it. The same import and publish checks run as in the in-game dialog. A shell with the same title is never created twice |
| `shellcreator republish <id> <file>` | Server console | Publishes the layout in such a JSON file as a new revision of an existing shell; its id, title, owner, tags and revision history stay. Refused while the shell is open in the editor                                                                                                                                                       |

The command names follow `Config.Command` and `Config.ExitCommand`. The subcommands of the console version are ignored when typed by a player; a player typing `/shellcreator kit` simply opens the library.

#### Key bindings

Key bindings are registered with FiveM and can be rebound by every player under Settings, Key Bindings, FiveM.

| Binding                | Default                       | What it does                                                                                                 |
| ---------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `aty_sc_use`           | `E` (`Config.Teleporter.key`) | Use a shell entrance or exit. Acts only at a teleporter, never while the editor is open or a panel has focus |
| `+aty_sc_preview_exit` | `BACKSPACE` (hold)            | Leave the walk-through preview; hold for `Config.Editor.previewKeyHoldMs`                                    |
| `+aty_sc_gizmo_toggle` | `R`                           | Switch the move and rotate handles of the gizmo                                                              |
| `+aty_sc_gizmo_select` | `MOUSE_LEFT`                  | Drag a gizmo handle                                                                                          |

Inside the editor the defaults of `Config.Shortcuts` apply:

| Action  | Key | Action              | Key                            |
| ------- | --- | ------------------- | ------------------------------ |
| Select  | `Q` | Rotate              | `R`                            |
| Floor   | `F` | Rotate left / right | `[` / `]`                      |
| Wall    | `C` | Undo / redo         | `Ctrl+Z` / `Ctrl+Y`            |
| Opening | `O` | Copy / cut / paste  | `Ctrl+C` / `Ctrl+X` / `Ctrl+V` |
| Stair   | `J` | Duplicate           | `Ctrl+D`                       |
| Paint   | `P` | Mirror X / Y        | `Ctrl+M` / `Ctrl+Shift+M`      |
| Decor   | `V` | Level up / down     | `PageUp` / `PageDown`          |

\| Light | `;` | View mode | `\` | | Anchor | `,` | Reset view | `Home` | | Room | `.` | Plan view | `End` | | Measure | `/` | Preview | `Insert` | | Demolish | `Delete` | Leave preview | `Backspace` | | Frame | `Ctrl+F` | | |

**Mouse wheel while placing.** When the active tool is placing something, the wheel turns it instead of zooming:

| Placing                                                           | Wheel                        | `Shift` + wheel            |
| ----------------------------------------------------------------- | ---------------------------- | -------------------------- |
| Floor or ceiling decor, anchors (spawn, exit and the other types) | 15° a notch, on the 15° grid | 5° a notch, on the 5° grid |
| Stairs, a pasted region                                           | A quarter turn (90°) a notch | Same                       |

Wheel up turns the item left (counter-clockwise). `R` still turns 90° and `Shift+R` turns 15°. `Ctrl` + wheel zooms the camera while you place.

The wheel zooms the camera again in these cases: while a gizmo is editing an item, in demolish mode, when no item is armed for placement, for wall-only decor (it keeps the wall face heading), in the opening tool, in read-only sessions, in the walk-through preview, and during the thumbnail photo.

**Outlines.** Only decor items that are hovered or selected and currently visible get the engine outline. Nothing is outlined in the preview, during the photo, while the editor is loading or closing, or for items on a level the view hides. Lights and anchors are marked with wire boxes, not outlines.

### Permissions

`Config.Permissions` has one rule per capability. A rule is `'admin'`, `'everyone'`, `'nobody'`, or a table. Admins (see Admin permission) pass every rule. A table rule passes when **any** listed criterion matches:

| Criterion                     | Meaning                                                                                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jobs = { [job] = minGrade }` | The player has that job at that grade or higher (`aty_lib` job lookup). With `onDuty = true` on the rule, or `Config.RequireDuty = true`, the player must also be on duty |
| `ace = 'name'`                | The player holds that ACE permission                                                                                                                                      |
| `housingAbility = 'name'`     | `aty_housing` reports that realtor ability for the player (its `HasRealtorAbility` export). `false` when `aty_housing` is not started                                     |
| `owner = true`                | Counts only in checks on one specific shell: the player owns it                                                                                                           |

Defaults (job `realestate`; grades are the minimum grade):

| Capability    | Default        | What it allows                                                        |
| ------------- | -------------- | --------------------------------------------------------------------- |
| `open`        | `realestate` 1 | Open the shell library                                                |
| `create`      | `realestate` 1 | Create shells                                                         |
| `editOwn`     | `realestate` 1 | Edit own shells (as owner, or as a member with the editor role)       |
| `editAny`     | admin          | Edit any shell                                                        |
| `publish`     | `realestate` 2 | Publish a revision                                                    |
| `deleteOwn`   | `realestate` 2 | Archive or delete own shells                                          |
| `deleteAny`   | admin          | Archive, delete or restore any shell                                  |
| `viewAll`     | `realestate` 1 | See public shells of other users                                      |
| `moveCoords`  | admin          | Place or move a coordinate-mode shell in the world                    |
| `teleporters` | admin          | Configure shell teleporters                                           |
| `import`      | admin          | Import a shell from JSON or a share code                              |
| `export`      | `realestate` 2 | Export a shell                                                        |
| `revisions`   | `realestate` 2 | Browse, restore and pin revisions                                     |
| `members`     | `realestate` 2 | Manage the member list of an own shell (the shell owner, or an admin) |
| `forceUnlock` | admin          | Take over a shell locked by another editor                            |
| `purge`       | admin          | Permanently delete a deleted shell                                    |

Shells are designed by admins and realtors; there are no player-owned shells by default. Other rule values are supported by the code but only the defaults are described as intended.

How a shell is seen by a player:

* Admins, the owner and members always see it.
* Otherwise, with `Config.HideOthers = true` nobody else sees it.
* With `Config.HideOthers = false`, a `public` shell is visible with `viewAll`, a `team` shell is visible to players holding `open` whose job equals the shell's team job, and a `private` shell is visible only to owner, members and admins.
* A deleted shell is visible only to admins and its owner.

A player's capabilities are cached for 10 seconds, so a job change can take up to 10 seconds to apply. Every action is checked again on the server. Refused actions are written to the audit log, at most one row per player and action per minute.

### Kit resource

`aty_shellcreator_kit` holds the 3D pieces the builder places. It has no scripts: its `fxmanifest.lua` streams the models and textures (`stream/`) and ships `kit.json`, the list the builder reads. Everything in it is original ATY work (see its `LICENSE.md`); `kit.json` only names three base-game models by name, which every GTA V client already has: two door leaves (`door_house1`, `door_studio`) and a pillar decor item. Stairs, railing, frames and window inserts are own models.

What `kit.json` (kit version 7) defines:

| Item        | Count or value                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Grid        | Cell 2.0 m, storey 3.2 m, wall 0.2 m, slab 0.2 m                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Materials   | 67: 32 wall, 25 floor, 10 ceiling                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Part groups | Floor and ceiling plates, walls in 8 shapes (`full`, `half`, `low`, `arch`, `door`, `window`, `wide`, `hatch`), 8 wall skins, 3 corner fillers, 13 door leaves (11 own, 1.30 x 2.30 m, plus the 2 base-game leaves above), 4 opening frames (wide, wide glass, arch glass, hatch glass), 8 window designs (each with a glass and an opaque variant), 3 stair styles (wood, dark, concrete), 1 stairwell railing, void                                                                                                                                              |
| Own decor   | 1 entry (a pillar). Base-game decor from `data/decor_base.json` is merged in (`includeDecorBase`), 111 built-in fit-out entries in the base catalogue (ceiling lights, bathroom fixtures, fitted kitchen units, shelving, office, wall art). 100 of the 111 show a picture icon in the editor (the same thumbnails `aty_housing` uses for its Decor Studio catalogue, shipped in `data/thumbs/kit/`); the other 11 show a category glyph. Living furniture is not shell decor: with aty\_housing it comes as the property's furniture set (Decor Studio furniture) |
| Tints       | None (`tints.count = 0`); anchor models are not part of this kit version                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

`Config.Kit` chooses the source:

| Value                    | Behaviour                                                                                                                                                                                                                                          |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `'auto'` (default)       | Use `aty_shellcreator_kit` when it is started and its `kit.json` validates; otherwise fall back to `base_temp`                                                                                                                                     |
| `'aty_shellcreator_kit'` | Only the kit resource. If it is missing or invalid the builder stays disabled                                                                                                                                                                      |
| `'base_temp'`            | Only the temporary base-game kit. **This is a development-only kit and is not shipped**: `data/kits/base_temp.json` is excluded from the package, so on a purchased copy it does not exist and the builder stays disabled without the kit resource |

`Config.KitExtensions` is RESERVED for kits that extend the base kit; nothing reads it in this version.

A change of the kit while the server runs re-resolves the kit and fires the local event `aty_shellcreator:kitChanged`. Published shells are rebaked lazily on their next request, because the data hash includes the kit hash. A kit changed between restarts is detected at boot, and shell statistics are refreshed in the background.

Every player downloads the kit's stream files on connect. A client that is missing models answers `models_missing` when a shell is spawned and the shell does not build.

The kit's `LICENSE.md` states: copyright 2026 ATY, all rights reserved; every model, texture and archetype file is original, procedurally generated ATY work with no third-party or Rockstar textures or models included.

### Exports

Server exports are called as `exports.aty_shellcreator:<Name>(...)`. Every server export answers from memory unless marked "may yield"; before the resource is ready they return the value in brackets.

#### Server exports

| Export                              | Returns                      | Description                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ----------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ListShells(filter?)`               | array `[]`                   | Shells as items. `filter = { status = 'published' (default) \| 'archived' \| 'draft' \| 'any', owner, tag, coordMode }`. Item: `id`, `uid`, `title`, `status`, `rev`, `hash`, `tags`, `stats = { area, rooms, levels, props }`, `anchors`, `bounds = { min, max, radius }`, `hasPhoto`, `planLevels`, `updatedAt`, `deleted`, `broken`. Deleted shells are never listed. `broken` means the current revision cannot be read |
| `GetShell(id)`                      | item or `nil`                | The item plus `settings`, `owner_name`, `created_at`, `description`. Works for deleted shells too, so a stored `sc_<id>` can be resolved                                                                                                                                                                                                                                                                                    |
| `GetShellPayload(id, rev?)`         | table or `nil`               | Baked shell data for hosts that spawn shells themselves. May yield                                                                                                                                                                                                                                                                                                                                                          |
| `GetShellAnchors(id, rev?)`         | anchor set or `nil`          | Anchors of the current or the given revision. Other revisions may yield                                                                                                                                                                                                                                                                                                                                                     |
| `GetShellPlan(id, level)`           | table or `nil`               | Floor plan data of the current revision. May yield                                                                                                                                                                                                                                                                                                                                                                          |
| `GetShellThumb(id, kind, level?)`   | data URI or `nil`            | `kind` is `'photo'` or `'plan'`. May yield, cached for 5 minutes                                                                                                                                                                                                                                                                                                                                                            |
| `ContainsPoint(id, point, margin?)` | boolean                      | `point` (`{ x, y, z }` or array) is above an occupied cell of the current revision, within that level's clear height plus the margin. The vertical margin is capped at half a slab, so a roof counts as outside. `false` when unknown                                                                                                                                                                                       |
| `GetShellMask(id)`                  | table or `nil`               | The occupancy mask `ContainsPoint` reads, `{ levels = [{ l, minX, minY, w, d, bits }], cell, storey, clear, slab }`, for hosts that mirror the check on the client                                                                                                                                                                                                                                                          |
| `GetWorldPlacement(id)`             | `{ x, y, z, h }` or `nil`    | World placement of a coordinate-mode shell                                                                                                                                                                                                                                                                                                                                                                                  |
| `CanEdit(src, id)`                  | boolean                      | Whether the player may edit that shell                                                                                                                                                                                                                                                                                                                                                                                      |
| `GetPolicy(src, id)`                | table or `nil`               | The player's policy for that shell: `view`, `edit`, `publish`, `delete`, `restore`, `move`, `teleporter`, `export`, `revisions`, `members`, `unlock`, `purge`, `owner`, `member`, `admin`                                                                                                                                                                                                                                   |
| `OpenEditor(src, id?)`              | `true` or `false, key, text` | Opens the editor for that player. Without an id it opens the shell library. With an id the server checks the kit, the shell, the permission and the `canOpen` hook, then the player's client runs its normal open flow. `text` is the refusal in this resource's language                                                                                                                                                   |
| `IsEditing(src)`                    | shell id or `nil`            | The shell the player is editing                                                                                                                                                                                                                                                                                                                                                                                             |
| `GetKit()`                          | table or `nil`               | `{ id, version, hash, capabilities, tints }` of the active kit                                                                                                                                                                                                                                                                                                                                                              |
| `RegisterHook(name, fn)`            | hook id or `nil`             | See Hooks                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `RemoveHook(id)`                    | boolean                      | Removes a hook                                                                                                                                                                                                                                                                                                                                                                                                              |

Anchor format: each anchor is `{ id, floor = { x, y, z, h }, ped = { x, y, z + 1.0, h }, label }` in shell-local metres. `ped` is the standing-ped reading: place a ped with `SetEntityCoords(ped, x, y, z - 1.0)`. An anchor set is `{ spawn, exit, stash = {...}, wardrobe = {...}, bed = {...}, kitchen = {...}, garage = {...}, custom = {...} }`.

```lua
-- list published shells and read one
local shells = exports.aty_shellcreator:ListShells({ tag = 'apartment' })
for _, s in ipairs(shells) do
    print(s.id, s.title, s.stats.area, s.stats.rooms)
end

local shell = exports.aty_shellcreator:GetShell(2)
local anchors = exports.aty_shellcreator:GetShellAnchors(2)
if anchors and anchors.spawn then
    print('spawn at', anchors.spawn.floor.x, anchors.spawn.floor.y, anchors.spawn.floor.z)
end

-- open the builder for a player
local ok, key, text = exports.aty_shellcreator:OpenEditor(source, 2)
if not ok then print('refused:', key, text) end
```

#### Client exports

Hosted shells: the host resource owns the routing bucket and the teleport; `aty_shellcreator` builds the props.

| Export                                     | Returns                                              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SpawnShell(id, opts)`                     | handle, or `false, reason[, missingModels]`          | Builds a shell. `opts = { origin = vector3 or { x, y, z }, heading = 0.0, rev = nil (current published), timeoutMs = 15000 (one deadline for data, models and collision, 1000 to 60000), hash = nil (a known data hash lets a cached copy skip the server round trip) }`. Yields. Reasons: `sc_invalid_input`, `sc_not_ready`, `sc_no_permission`, `sc_not_found`, `sc_shell_broken` (from the server), `sc_payload_timeout`, `models_missing`, `prop_cap`, `pool_full`, `models_timeout`, `create_refused`, `cancelled`. Parts the client cannot create (slow, refused or missing models, door leaves without `game_enableDynamicDoorCreation`) do not stop the shell while the spawn point has a floor: they are printed once and retried in the background (see `notSpawned` and `issue` of `GetHandleInfo`) |
| `DespawnShell(handle)`                     | boolean                                              | Removes the shell                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `MoveShell(handle, origin, heading)`       | boolean                                              | Moves a built shell                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `GetHandleAnchors(handle)`                 | table or `nil`                                       | Anchor set in world space. Each anchor is `{ floor = vector4, ped = vector4 (z + 1.0), label, id }`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `GetHandleInfo(handle)`                    | table or `nil`                                       | `{ handle, id, rev, hash, owner, state, origin, heading, props, doors = { doorKey... }, notSpawned, issue }`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `IsInsideShell()`                          | `handle, shellId, roomIndex` or `nil`                | The shell the local player is in                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `GetCurrentRoom()`                         | `{ handle, id, room = { id, name, area } }` or `nil` | The room the local player is in                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `SetDoorLocked(handle, doorKey, locked)`   | boolean                                              | Lock or unlock a door leaf                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `SetRoomLights(handle, roomId \| nil, on)` | boolean                                              | Switch the lights of one room, or of the whole shell with `nil`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `OpenEditor(id?)`                          | boolean                                              | Asks the server to open the editor (`true` = asked, permission is checked there); no id opens the shell library. `false` while an editor is already active or opening                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `IsEditorActive()`                         | boolean                                              | Whether the local player is in the editor                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

A handle keeps the revision it was built with; a newer published revision is used by the next `SpawnShell`. Under the client prop cap, coordinate shells farther away are removed first. Handles are removed when the resource that created them stops.

```lua
-- client: build a shell at a spot and walk the ped to its spawn anchor
local handle, reason = exports.aty_shellcreator:SpawnShell(2, {
    origin = vector3(-3500.0, -3500.0, 900.0), heading = 0.0,
})
if not handle then
    print('could not build shell:', reason)
    return
end
local a = exports.aty_shellcreator:GetHandleAnchors(handle)
if a and a.spawn then
    SetEntityCoords(PlayerPedId(), a.spawn.ped.x, a.spawn.ped.y, a.spawn.ped.z - 1.0)
end
-- later
exports.aty_shellcreator:DespawnShell(handle)
```

### Events

Server events are local events (`TriggerEvent`). Listen with `AddEventHandler` only, never `RegisterNetEvent`, so a client can never fake them.

#### Server events

| Event                           | Arguments       | When                                                                                                                                                         |
| ------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `aty_shellcreator:ready`        | none            | Boot finished. Re-query and register your hooks again                                                                                                        |
| `aty_shellcreator:shellChanged` | `id, info`      | `info.kind` is `published`, `archived`, `deleted`, `restored`, `renamed`, `moved`, `teleporter`, `purged` or `thumbnail`; `info.rev` is the current revision |
| `aty_shellcreator:editorOpened` | `src, id`       | A player opened the editor on a shell                                                                                                                        |
| `aty_shellcreator:editorClosed` | `src, id, mode` | The session ended                                                                                                                                            |
| `aty_shellcreator:kitChanged`   | `kitHash`       | The active kit changed while the server runs                                                                                                                 |

```lua
AddEventHandler('aty_shellcreator:shellChanged', function(id, info)
    if info.kind == 'published' then
        print(('shell %d now at revision %s'):format(id, tostring(info.rev)))
    end
end)
```

#### Client events

| Event                                  | Arguments    | When                                                                                                                                                                  |
| -------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aty_shellcreator:client:enteredShell` | `handle, id` | The local player entered a built shell                                                                                                                                |
| `aty_shellcreator:client:leftShell`    | `handle, id` | The local player left it                                                                                                                                              |
| `aty_shellcreator:client:handlesLost`  | `handles`    | Fired synchronously while this resource stops, with every handle that is about to disappear. Set flags only and act in your own thread; the ped is frozen when inside |
| `aty_shellcreator:client:editorOpened` | `id`         | The local editor opened                                                                                                                                               |
| `aty_shellcreator:client:editorClosed` | `id`         | The local editor closed                                                                                                                                               |

Other `aty_shellcreator:client:*` and `aty_shellcreator:server:*` net events (`openManager`, `openEditor`, `payload`, `index`, `indexDelta`, `forceExit`, `forceClose`, `editorLoad`, `upload`) are internal to the resource. Use the exports instead of triggering them.

### Hooks

Register a hook with `exports.aty_shellcreator:RegisterHook(name, fn)`. Hooks are removed when the registering resource stops, so register them again on `aty_shellcreator:ready`. A hook that can veto returns `false, message` (the message is shown to the player, at most 128 characters); anything else allows.

| Hook               | Data                                 | Behaviour when your function errors                                                                                    |
| ------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `canOpen`          | `{ source, shellId, charId }`        | Allows (fails open)                                                                                                    |
| `canPublish`       | `{ source, shellId, charId, stats }` | Allows (fails open)                                                                                                    |
| `canArchive`       | `{ source, shellId, force }`         | Refuses, unless `force` is set (admins only)                                                                           |
| `canDelete`        | `{ source, shellId, force }`         | Refuses, unless `force` is set (admins only)                                                                           |
| `canUseTeleporter` | `{ source, shellId, direction }`     | Refuses (fails closed)                                                                                                 |
| `usage`            | `{ shellId }`                        | Return `{ resource, count, label }`; shown in the manager and the details view. A failing or malformed hook is skipped |
| `onPublished`      | `{ shellId, rev }`                   | Notification only, runs in its own thread                                                                              |
| `onDeleted`        | `{ shellId, rev }`                   | Notification only, runs in its own thread                                                                              |

```lua
-- refuse to archive or delete a shell your resource still uses
local function inUse(data)
    if countMyUses(data.shellId) > 0 then
        return false, 'Used by my resource'
    end
end

local function register()
    exports.aty_shellcreator:RegisterHook('canArchive', inUse)
    exports.aty_shellcreator:RegisterHook('canDelete', inUse)
end
register()
AddEventHandler('aty_shellcreator:ready', register)
```

### aty\_housing integration

`aty_housing` is optional. When both run, every shell published in the Shell Creator becomes a housing shell interior named `sc_<id>`. This is handled by `aty_housing` (`server/built_shells.lua`) through the exports, events and hooks above; this resource needs nothing extra.

**The interface follows the integration.** The builder asks the server whether `aty_housing` is started and shows housing-specific parts only while it runs: the housing wording in the guide and the details tabs, the `host` export format, and the `housingAbility` chip of the permission rules. Without `aty_housing` the same places use generic wording about "host scripts" that read published shells through the exports, and the housing-only options are hidden. The state updates live when `aty_housing` starts or stops; nothing has to be configured.

What `aty_housing` takes from a published shell:

| From the shell                       | Used for                                                                                                                                                                   |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spawn` and `exit` anchors           | Where a player appears when entering, and the inside exit. A shell with no spawn anchor gets a fallback point at the grid origin and a warning in the housing health check |
| `stash` anchors                      | Property stashes (the first is the main stash; more become extra stashes, limited by `Config.Stashes.max` in `aty_housing`)                                                |
| `wardrobe`, `bed`, `kitchen` anchors | Living points, mapped through `Config.BuiltShells.fixtures` (default `wardrobe` to wardrobe, `bed` to bed, `kitchen` to stove)                                             |
| Tags `apartment`, `house`, `trailer` | The listing type (`Config.BuiltShells.typeTag`)                                                                                                                            |
| Stats and thumbnails                 | Area, rooms, levels, props and the photo and floor plan gallery of a listing                                                                                               |
| Bounds and occupancy mask            | The inside check and the point check for captured points and furniture (`ContainsPoint`)                                                                                   |

The interior is built on the client at `Config.ShellSpawn` of `aty_housing` through `SpawnShell`, in the property's own routing bucket. That spawn point is marked in `aty_housing` as not verified in game: change it if a shell clips into anything.

The `Config.BuiltShells` block of `aty_housing` (`shared/config.lua`) controls the integration:

| Option              | Default                                                             | Description                                                                                                                                                                              |
| ------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`           | `'auto'`                                                            | `'auto'` uses the Shell Creator when it runs and stays quiet otherwise; `true` does the same but a missing resource is an error in the console and the admin health check; `false` never |
| `resource`          | `'aty_shellcreator'`                                                | Resource name of the Shell Creator                                                                                                                                                       |
| `planImages`        | `true`                                                              | Show the photo and floor plans in the listing gallery                                                                                                                                    |
| `typeTag`           | `{ apartment = 'apartment', house = 'house', trailer = 'trailer' }` | Shell tag to listing type                                                                                                                                                                |
| `fixtures`          | `{ wardrobe = 'wardrobe', bed = 'bed', kitchen = 'stove' }`         | Anchor type to living point type; `false` = anchors give no living points                                                                                                                |
| `containsMargin`    | `0.5`                                                               | Metres around the shell's floor cells that still count as inside                                                                                                                         |
| `radiusPad`         | `2.0`                                                               | Metres added to the shell's bounding radius                                                                                                                                              |
| `spawnTimeoutMs`    | `15000`                                                             | How long a client waits for the shell to build (1000 to 60000)                                                                                                                           |
| `blockDelete`       | `true`                                                              | Refuse archiving or deleting a shell that properties use. An admin can force it                                                                                                          |
| `blockEditorInside` | `true`                                                              | Refuse opening the Shell Creator while the player is inside a property or on a creator visit                                                                                             |
| `notifyUpdate`      | `true`                                                              | Players inside hear once that a new version was published; it applies when the property is empty again                                                                                   |
| `notifyOwners`      | `true`                                                              | Owners and tenants are notified when their interior is archived, deleted or broken                                                                                                       |
| `thumbCacheSeconds` | `300`                                                               | How long photo and floor plan images are cached on the server                                                                                                                            |

Behaviour to know:

* `aty_housing` registers the `canArchive`, `canDelete`, `usage` and `canOpen` hooks. The manager shows how many properties use a shell.
* A shell that disappears (stopped, archived, deleted, purged, broken, unpublished) stays registered as unavailable, so its properties show "interior unavailable" instead of vanishing. Entering is refused, and such a property cannot be bought until the interior is back.
* `aty_housing` keeps the last known copy of every shell in its own table `aty_housing_built_shells`, so labels and points survive while `aty_shellcreator` is not running.
* A new revision does not change a property while someone is inside; it applies on the next entry.
* Publish a shell, then pick it under built shells in the `aty_housing` property creator. The creator can also open the builder for a shell (`OpenEditor`).

### Features

#### Builders (realtors and admins)

* Build on a 2 m grid with 3.2 m storeys: up to 6 levels including basements, shells of up to 64 x 64 m.
* Tools for floors, walls in 8 shapes, openings (doors, windows, wide openings), stairs, ceilings, painting (material and surface), decor, lights, anchor points, room naming and atmosphere, and a measure tool.
* Mouse-wheel rotation while placing decor, anchors, stairs and pasted regions (15° a notch, `Shift` 5°; `Ctrl` + wheel zooms), see Key bindings.
* Select, copy, cut, paste, duplicate and mirror regions; rotate; deep undo and redo; save regions as snippets and paste them into other shells.
* Start from a blank shell or one of seven templates: studio, one bedroom, two bedroom, office, shop, garage and warehouse.
* Prop, decor and light budgets with a live warning; publishing is blocked when a cap is exceeded.
* Publish validation with errors (no spawn point, spawn or exit not on a real floor or with less than 2.0 m headroom, over the caps, layout integrity, no real floor, empty title) and warnings (for example door swings over stairs or decor, perimeter openings or glass).
* Walk-through preview of the built shell before publishing.
* 16 atmosphere presets with clock and weather locks, per shell and per room.
* Orbit, plan and fly camera, level views, and an automatic photo and floor plan per level when you publish.
* Autosaved drafts, revision history with notes and pinned revisions, restore a revision as a draft.
* Share a shell as JSON or a share code; import by pasting one (the import checks size, grid version and kit keys, and reports what was replaced or dropped).
* Optional shell members (editor or viewer) so a colleague can work on your shell.

#### Admins

* Edit, publish, archive, delete, restore and permanently delete any shell.
* Visit any shell with `/shellcreator tp <id>` and return with `/shellcreator tp`.
* Take over a locked shell, or clear a stale lock from the console.
* Place coordinate-mode shells in the world with teleporter entrances (marker and optional target zone, per-player or shared instance, optional vehicle entry for the driver, restricted access with an intrusion watchdog).
* Import shells from files with the console `import` command; run `validate`, `rebake` and `kit` for diagnostics.
* Audit log of opens, publishes, deletions, unlocks, imports and refused actions.

#### Server owners

* No SQL import; tables are created and migrated on start.
* Permissions per capability by job and grade, ACE, duty state, or the housing realtor ability; hide other users' shells.
* Per-shell prop, decor, light and anchor caps and a global client prop cap to protect performance.
* Rate limits on every call, server-side re-validation of every edit, a server-authoritative editor lock and draft storage.
* Every value the client sends is checked on the server; the client can never send models or prop lists.
* Open integration API (exports, events, hooks) for your own scripts, and an interface in English, Turkish, German, French, Spanish and Brazilian Portuguese.

### Languages

| Language                    | `Config.Language` | Files                                       |
| --------------------------- | ----------------- | ------------------------------------------- |
| English (default, fallback) | `'en'`            | `locales/en.lua`, `locales/ui/en.lua`       |
| Turkish                     | `'tr'`            | `locales/tr.lua`, `locales/ui/tr.lua`       |
| German                      | `'de'`            | `locales/de.lua`, `locales/ui/de.lua`       |
| French                      | `'fr'`            | `locales/fr.lua`, `locales/ui/fr.lua`       |
| Spanish                     | `'es'`            | `locales/es.lua`, `locales/ui/es.lua`       |
| Brazilian Portuguese        | `'pt-br'`         | `locales/pt-br.lua`, `locales/ui/pt-br.lua` |

To switch, set `Config.Language` in `shared/config.lua` to the code in the second column and restart the resource. Server messages and the interface follow that setting. A key that is missing in the chosen language is shown in English.

### Troubleshooting

| Problem                                                                                                                                | Solution                                                                                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Console: `no usable shell kit - the builder stays disabled`, or players get "No shell kit is installed. Start aty\_shellcreator\_kit." | Add `ensure aty_shellcreator_kit` before `ensure aty_shellcreator`, check the folder name is exactly `aty_shellcreator_kit`, and run `shellcreator kit` in the console. The error line names the reason for every kit source that was tried                                             |
| Doorways without door leaves; F8 `not spawned (doors_disabled): aty_sc_door_...`                                                       | FiveM refuses to create door leaves unless `setr game_enableDynamicDoorCreation "true"` is set. `Config.Doors.enableDynamicDoors = true` (default) sets it when the resource starts; add the line to `server.cfg` to make it permanent. The rest of the shell spawns without the leaves |
| `Config.Kit = ... is not a kit source` in the console                                                                                  | Use `'auto'`, `'aty_shellcreator_kit'` or `'base_temp'`. `'base_temp'` does not exist in a purchased copy                                                                                                                                                                               |
| `/shellcreator` says "You are not allowed to do that."                                                                                 | The player lacks the `open` capability. Give them the job and grade of `Config.Permissions.open`, or add the ACE line. Job changes can take up to 10 seconds to apply                                                                                                                   |
| Admin cannot do admin-only things                                                                                                      | Check `Config.AdminPermission` for your framework and the ACE line `add_ace group.admin aty_shellcreator.admin allow` plus the `add_principal`. On qb-core `group.admin` is not a permission level. On a framework other than `qb-core` and `es_extended` only ACE works                |
| A shell stays locked after a crash                                                                                                     | The lock expires after `Config.Editor.lockTimeoutSeconds` (90 s) without a heartbeat. To clear it at once, run `shellcreator unlock <id>` in the console or use an admin account                                                                                                        |
| Stuck in the editor, preview or with a panel on screen                                                                                 | Type `/shellcreatorexit` (or the name in `Config.ExitCommand`)                                                                                                                                                                                                                          |
| A shell does not build for a player, `models_missing`                                                                                  | The kit's stream files did not reach the client. Check `aty_shellcreator_kit` is started and not blocked in your stream setup, and have the player reconnect                                                                                                                            |
| Shell does not build: `prop_cap` or `pool_full`                                                                                        | The client hit `Config.Runtime.maxClientProps` or the game object pool limit. Lower `Config.Budget.maxProps` for new shells or reduce how many shells are built at once                                                                                                                 |
| `models_timeout` or `sc_payload_timeout`                                                                                               | Slow client or heavy server. Raise `Config.Runtime.modelTimeoutMs` / `payloadTimeoutMs`, or the host's own timeout (`Config.BuiltShells.spawnTimeoutMs` in `aty_housing`)                                                                                                               |
| Publish is refused                                                                                                                     | Open the validation list in the editor. Common causes: no spawn anchor, spawn or exit not on a floor or without headroom, a cap exceeded, empty title. Run `shellcreator validate <id>` for the codes                                                                                   |
| A shell card is marked broken                                                                                                          | Its current revision cannot be read. Restore an older revision as a draft and publish it; until then `GetShellPayload` returns `nil` and `aty_housing` treats the interior as unavailable                                                                                               |
| No photo thumbnails                                                                                                                    | Install and start `screenshot-basic`, and keep `Config.Thumbnails.photo` at `'auto'`. Floor plan images do not need it                                                                                                                                                                  |
| A property in `aty_housing` says "interior unavailable"                                                                                | The shell is stopped, archived, deleted or broken. Start `aty_shellcreator`, restore or republish the shell, or check the reason in the `aty_housing` admin health check                                                                                                                |
| Weather or time is wrong inside shells                                                                                                 | Your weather sync script is fighting the shell's locks. Pause it with `Config.Atmosphere.pauseEvents`                                                                                                                                                                                   |
| Console: `Config.Storage changed ... coordinate shells moved` or `coordinate shells ... overlap`                                       | Restore the previous `Config.Storage` values, or move one of the overlapping shells                                                                                                                                                                                                     |
| Database errors at start                                                                                                               | Check `oxmysql` (or your database resource) and the connection string; the migration retries three times, then the resource stays idle until restarted                                                                                                                                  |

### Known limitations

* Only the parts listed as verified in Tested scope were seen working in game. Everything else is implemented and checked with offline test harnesses only.
* Only QBCore was used for live tests. ESX and Qbox are untested; on a framework other than `qb-core` or `es_extended` the framework admin check is skipped and only ACE applies.
* No player-owned shells in this version: by default only admins and realtors design shells. The permission rules can be loosened, but that setup is not tested.
* A newly published revision reaches players already inside a shell on their next entry; there is no live respawn.
* `Config.Editor.origin` and `Config.Storage.origin` were chosen far from the map and are not verified against map geometry in game. Move them if the surroundings clip or the area is not empty on your server.
* `Config.KitExtensions` and `Config.CustomTextures` are RESERVED and have no effect in this version. The current kit has no colour tints and no anchor models.
* Photo thumbnails need `screenshot-basic`.
* Each shell is limited by the budget caps and, per client, by `Config.Runtime.maxClientProps`.
* Shell names and descriptions are free text typed by builders; the interface texts are available in English, Turkish, German, French, Spanish and Brazilian Portuguese.
* Job-based rules need the job lookup of `aty_lib`; without it only admin, ACE and housing-ability criteria can match.
* The seven sample shells (`Config.SampleShells`) are plain grid layouts made with the own kit; the two furnished houses get their furniture from `aty_housing` furniture sets (matched by title), not from the layout.

### Third-party notices

Third-party components shipped with or required by these resources are listed in LICENSES.md: the Outfit typeface (SIL Open Font License 1.1, text in `docs/OFL.txt`), Vue and Pinia (MIT) bundled into the pre-built UI, and the separately installed resources `aty_lib`, `aty_shellcreator_kit`, `aty_housing`, `aty_clothing` and `screenshot-basic`. `aty_shellcreator` ships no Rockstar Games assets and no third-party shell, MLO or housing code. The kit's own licence statement is in `aty_shellcreator_kit/LICENSE.md`.
