# esx_lscustom (/docs/addons/esx_lscustom)

esx_lscustom brings the Los Santos Customs workshop to ESX Legacy. Players drive into one of the workshops, press the interaction key and customize performance, body parts, wheels, colors, lights and more in a NUI with a cart, a camera mode and live vehicle stats. Everything in the cart is priced and checked on the server before money is taken.

## Overview [#overview]

| | |
| --- | --- |
| Resource | `esx_lscustom` (version `1.0`) |
| Side | Client + server, NUI in `html/` (no build step) |
| Dependencies | `esx_lib`, `es_extended`, `oxmysql` |
| Reads | `vehicles` table from [esx_vehicleshop](/docs/addons/esx_vehicleshop) for base prices, `owned_vehicles` for saving |
| Optional | `esx_addonaccount` with `society_mechanic` (mechanic-only mode) |
| SQL file | None |

How it works:

1. The player sits in a vehicle within `Config.DrawDistance` of a zone and presses `E`.
2. The client sends `esx_lscustom:startModing` with the current vehicle properties. The server stores this snapshot as the session's reference.
3. The player previews parts. Nothing is paid until checkout.
4. On checkout, the `esx_lscustom:buyCart` callback recomputes every cart line from `Config.Menus` and the vehicle price, checks that the vehicle props only differ in paid fields, charges the money and saves the result.
5. On cancel or disconnect the original props are restored.

## Installation [#installation]

<Steps>
<Step>
### Install the vehicle catalog [#install-the-vehicle-catalog]

Prices depend on the `vehicles` table. Install [esx_vehicleshop](/docs/addons/esx_vehicleshop) or create that table with `model` and `price` columns yourself.
</Step>
<Step>
### Ensure the resource [#ensure-the-resource]

```bash title="server.cfg"
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_vehicleshop
ensure esx_lscustom
```
</Step>
</Steps>

## Configuration [#configuration]

| Key | Default | Description |
| --- | --- | --- |
| `Config.DrawDistance` | `10.0` | Distance at which the zone hint appears. |
| `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. |
| `Config.IsMechanicJobOnly` | `false` | Only the `mechanic` job can use the workshop. Payment comes from `society_mechanic`. |
| `Config.Currency` | `'$'` | Currency symbol in messages and UI. |
| `Config.Workshop.UseNui` | `true` | NUI workshop. `false` uses the ESX default menu flow. |
| `Config.Workshop.EnableCamera` | `true` | Camera controls in the NUI. |
| `Config.Workshop.EnableStats` | `true` | Live handling stats. |
| `Config.Workshop.PurchaseCooldown` | `1500` | ms between checkouts per player. |
| `Config.Workshop.Ownership.RequireOwned` | `false` | Only accept vehicles owned by the buyer (or mechanic customer vehicles). |
| `Config.Workshop.Ownership.SaveOwnedVehicles` | `true` | Save paid changes to `owned_vehicles`. |
| `Config.Workshop.Ownership.AllowMechanicCustomerVehicles` | `false` | Mechanics may tune and save vehicles they do not own. |
| `Config.Workshop.StatGains` | per mod | Display-only stat gains, e.g. `modEngine = { accel = 12, speed = 8 }`. |
| `Config.Zones` | `ls1` ... `ls5` | Workshop locations with `Pos`, `Name` (blip) and `Hint`. |
| `Config.Colors` | list | Paint color groups shown in the respray menu. |
| `Config.DefaultWheelsPriceMultiplier` | `2` | Wheel price in percent when a wheel menu has no `price`. |
| `Config.Menus` | large table | Menu tree and prices. |

### Prices in `Config.Menus` [#prices-in-configmenus]

Every purchasable entry has a `modType` and a `price`. The price is a **percentage of the vehicle price**:

```lua title="config.lua"
modEngine = {
    label = TranslateCap('engine'),
    parent = 'upgrades',
    modType = 11,
    price = { 13.95, 32.56, 65.12, 139.53 }, -- one value per level
},
```

- Performance mods (engine, brakes, transmission, suspension, armor) take one percentage per level. Removing a mod (level `-1`) is free.
- Turbo and xenon toggles use a single percentage.
- Cosmetic parts, paint and wheels use a single percentage.

To make tuning cheaper or more expensive overall, scale these numbers. The workshop zones themselves look like this:

```lua title="config.lua"
Config.Zones = {
    ls1 = {
        Pos = vector3(-337.38, -136.92, 38.57),
        Name = TranslateCap('blip_name'),
        Hint = TranslateCap('press_custom'),
    },
}
```

## Developer API [#developer-api]

The resource has no exports.

| Name | Type | Description |
| --- | --- | --- |
| `esx_lscustom:startModing` | Server net event | `(props, netId)`. Starts a session for the vehicle the player is in. Ignored unless the player is within 12 m of a zone, in that vehicle, and matching model and plate. |
| `esx_lscustom:stopModing` | Server net event | `(plate)`. Ends the player's session for that plate. |
| `esx_lscustom:buyCart` | Server callback | `(payload, netId)` with `payload.cart` and `payload.vehicleProps`. Returns `success, message`. Max 40 cart lines. |
| `esx_lscustom:getVehiclesPrices` | Server callback | Returns the cached `{ model, price }` list. |
| `esx_lscustom:setVehiclesPrices` | Client event | Server pushes the price list (refreshed every 10 minutes when changed). |
| `esx_lscustom:restoreMods` | Client event | `(netId, props)`. Restores a vehicle when its customer disconnects mid-session. |
| `esx_lscustom:cancelInstallMod` | Client event | Restores the original props of the current vehicle. |

<Callout type="info" title="Client-triggerable">
`startModing` and `buyCart` are callable by clients. The server ignores the client's prices: it recalculates each line from `Config.Menus`, rejects props that changed fields nobody paid for, enforces the distance, the session and a cooldown, and locks per player and plate during checkout. If you add custom mods, add them to `Config.Menus` so they are validated too.
</Callout>

## Common issues [#common-issues]

- **Everything costs the same for every car.** The model is missing in `vehicles`, so the 50000 fallback is used. Add the model to the table.
- **Changes are lost after restart.** The vehicle is not in `owned_vehicles`, the buyer is not the owner, or `SaveOwnedVehicles` is `false`. Your garage script must also apply the stored props.
- **"Not enough money" in mechanic-only mode.** Payment comes from `society_mechanic`, not the mechanic's wallet.
- **Hint does not appear.** You must be in a vehicle, and with `IsMechanicJobOnly` you need the `mechanic` job.

## Related [#related]

- [esx_mechanicjob](/docs/addons/esx_mechanicjob)
- [esx_vehicleshop](/docs/addons/esx_vehicleshop)
- [esx_garage](/docs/addons/esx_garage)
- [esx_addonaccount](/docs/addons/esx_addonaccount)
