# esx_property (/docs/addons/esx_property)

esx_property is the official housing resource for ESX Legacy. Players buy properties from a list of 95 premade houses and apartments, enter them in their own routing bucket, give keys to friends, place furniture, store vehicles in a garage and watch a CCTV camera. Admins and an optional real estate job can create, edit and sell properties in-game.

## Overview [#overview]

| | |
| --- | --- |
| Resource | `esx_property` (version `2.0`) |
| Side | Client + server |
| Dependencies | `esx_lib`, `es_extended`, `oxmysql` |
| Optional | `ox_inventory` (stash), `esx_datastore` + `skinchanger` (wardrobe), `esx_society` + `esx_addonaccount` (real estate job), `esx_context`, `esx_notify`, `esx_progressbar` (raids), IPL or shell resources |
| SQL file | None. Tables are created at startup |
| Storage | `esx_properties`, `esx_property_migrations` |

Features in short:

- **Buying and selling.** Buying takes the price from the bank. Selling back pays 60 % of the price.
- **Keys.** The owner can give and remove keys. Players with keys can enter and use the property.
- **Locking.** Locked doors keep visitors out. `Config.CanAlwaysExit` and `Config.OwnerCanAlwaysEnter` control edge cases. Visitors can knock.
- **Furniture.** Furniture stores sell props by category, paid from the bank, placed and moved with keyboard controls.
- **Garage.** Store owned vehicles inside a property and take them out again.
- **CCTV.** A camera above the door with zoom and night vision. Optional Discord webhook for screenshots.
- **Raids.** On-duty `police` (and optionally admins) can break in, optionally consuming an item.
- **Interiors.** IPL interiors are on by default. Shell interiors need `Config.Shells = true` and a shell pack.

## Installation [#installation]

<Steps>
<Step>
### Ensure the resource [#ensure-the-resource]

```bash title="server.cfg"
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_datastore
ensure esx_society
ensure ox_inventory
ensure esx_property
```

Only `oxmysql`, `esx_lib` and `es_extended` are required. Start optional resources before `esx_property`.
</Step>
<Step>
### First start: JSON to SQL migration [#first-start-json-to-sql-migration]

On startup the resource creates `esx_properties` and `esx_property_migrations` (both must be InnoDB). If the migration `properties_json_to_sql_v1` has not run yet, it reads `properties.json`, imports every property into SQL in one transaction and keeps the JSON file unchanged.

The import refuses to run if `esx_properties` already has rows without a migration marker, or if `properties.json` is missing or empty. While storage is not ready, all property actions are rejected.
</Step>
<Step>
### Optional setup done automatically [#optional-setup-done-automatically]

The resource inserts a shared `property` datastore if the `datastore` table exists. With `Config.PlayerManagement.Enabled` it also creates the real estate job, its grades and the society account, and registers the society with esx_society.
</Step>
</Steps>

## Configuration [#configuration]

### General [#general]

| Key | Default | Description |
| --- | --- | --- |
| `Config.ForSaleBlips` | `false` | Blips for properties for sale (can clutter the map). |
| `Config.OwnedBlips` | `true` | Blips for owned properties. |
| `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. |
| `Config.Shells` | `false` | Enable shell interiors. |
| `Config.SaveInterval` | `5` | Fallback save interval in minutes. Changes are also saved after a short debounce. |
| `Config.MaxFurniturePerProperty` | `500` | Limit for new furniture purchases. |
| `Config.MaxVehiclesPerProperty` | `100` | Limit for new garage deposits. |
| `Config.CanAlwaysExit` | `true` | `false` allows players to be locked in. |
| `Config.OwnerCanAlwaysEnter` | `true` | `false` means the owner needs the door unlocked. |
| `Config.MaxNameLength` | `20` | Max length of property names. |
| `Config.CanCustomiseInventoryAndWardrobePositions` | `true` | Owners can move storage and wardrobe points. |
| `Config.WipeCustomNameOnSell` | `true` | Reset the custom name on sale. |
| `Config.WipeFurnitureOnSell` | `true` | Remove furniture on sale. |
| `Config.AllowedGroups` | `{ "admin" }` | ESX groups with admin access. |
| `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Uses ox_inventory stashes. |

### Prices, raids, garage, logs [#prices-raids-garage-logs]

| Key | Default | Description |
| --- | --- | --- |
| `Config.ZonePriceOptions.Enabled` | `true` | Suggest prices by zone in the creator. |
| `Config.ZonePriceOptions.Default` | `250000` | Base price for suggestions. |
| `Config.ZonePrices` | `[0] = 10` ... `[5] = 1` | Multiplier per zone class (posh to scum). |
| `Config.Raiding.Enabled` | `true` | Allow raids. |
| `Config.Raiding.CanAdminsRaid` | `true` | Admins may raid too. |
| `Config.Raiding.ItemRequired` | `{ RemoveItem = true, name = "bread", label = "Bread", ItemCount = 1 }` | Item needed to raid. Remove the table to disable. Change `bread` to a real tool. |
| `Config.Raiding.Animation` | `{ type = "Scenario", Scenario = "WORLD_HUMAN_HAMMERING" }` | Raid animation. |
| `Config.Garage.Enabled` | `true` | Property garages. |
| `Config.Garage.OwnedVehiclesOnly` | `true` | Only owned vehicles can be stored. |
| `Config.Garage.MySQLquery` | ``"UPDATE `owned_vehicles` SET `stored` = ? WHERE `plate` = ?"`` | Query run when storing or retrieving. Adapt it to your garage schema. |
| `Config.Logs.Webhook` | `""` | Discord webhook for logs (server only). |
| `Config.Logs.LogLevel` | `1` | `0` none, `1` major, `2` major + minor, `3` all. |

### Furniture and CCTV [#furniture-and-cctv]

| Key | Default | Description |
| --- | --- | --- |
| `Config.Furniture.Enabled` | `true` | Furniture system. |
| `Config.Furniture.RotationSpeed` / `MovementSpeed` / `MovementZspeed` | `0.4` / `0.01` / `0.05` | Placement speeds. |
| `Config.Furniture.Controls` | control IDs | Arrow keys move, Alt/Shift rotate, Enter confirms, Backspace exits. |
| `Config.FurnitureStores` | Ikea, Electronics Store | Stores and the categories they sell. |
| `Config.FurnitureCatagories` | list | Props per category with prices. |
| `Config.CCTV.Enabled` | `true` | CCTV system. |
| `Config.CCTV.PictureWebook` | `""` (server) | Discord webhook for CCTV screenshots. Leave the client string unchanged. |
| `Config.CCTV.HeightAboveDoor` / `FOV` | `2.5` / `80.0` | Camera placement. |
| `Config.CCTV.MaxLeftRotation` / `MaxRightRotation` / `MaxUpRotation` / `MaxDownRotation` | `80` / `-50` / `10` / `-45` | Rotation limits. |
| `Config.CCTV.MaxZoom` / `MinZoom` / `RotateSpeed` | `30` / `100` / `0.3` | Zoom and speed. |

### Real estate job (`Config.PlayerManagement`) [#real-estate-job-configplayermanagement]

| Key | Default | Description |
| --- | --- | --- |
| `Enabled` | `false` | Enable the real estate job. |
| `job` / `joblabel` | `"realestateagent"` / `"Estate Agent"` | Job name and label. |
| `society` | `"society_realestateagent"` | Society account. |
| `SalePercentage` | `0.25` | Share of the price paid to the selling agent's bank. The rest goes to the society. |
| `jobRanks` | trainee (0), agent (1), boss (2) | Grades created automatically. |
| `Permissions` | e.g. `CreateProperty = 0`, `DeleteProperty = 2` | Minimum grade per action. |
| `Locations` | office entrance, exit, management menu | Real estate office points. |

`Config.Interiors` lists the IPL and shell interiors with their spawn, wardrobe and storage positions. `Config.WardrobeInteraction` is a client function you can replace to use your own clothing script.

## Commands [#commands]

| Command | Who | Description |
| --- | --- | --- |
| `/property:create` | Admins, or real estate agents with the `CreateProperty` grade | Opens the property creator. |
| `/property:admin` | `Config.AllowedGroups` | Admin menu: manage, teleport to or delete any property. |
| `/property:save` | `Config.AllowedGroups` | Saves all properties now. |
| `/property:refresh` | `Config.AllowedGroups` | Resends property data to all players. |
| `/getoffset` | Client command | Prints your offset inside a property to F8 (for wardrobe and storage positions). |

Command names come from the locale file and can differ if you change it.

## Exports [#exports]

### Server [#server]

| Export | Returns |
| --- | --- |
| `GetProperties()` | The full properties table, indexed by property ID. |
| `GetOwnedProperties()` | Array of owned properties. |
| `GetNonOwnedProperties()` | Array of properties for sale. |
| `GetPlayerProperties(identifier)` | Array of properties owned by that identifier. |
| `GetPropertyKeys(propertyId)` | Table of key holders, `{ [identifier] = true }`. |
| `DoesPlayerHaveKeys(propertyId, identifier)` | `true` / `false` (see note below). |
| `ForceSaveProperties()` | Saves immediately and logs the calling resource. |

```lua title="server/main.lua"
ESX.RegisterCommand('myhouses', 'user', function(xPlayer)
    local owned = exports.esx_property:GetPlayerProperties(xPlayer.getIdentifier())

    for _, property in ipairs(owned) do
        xPlayer.showNotification(property.setName ~= '' and property.setName or property.Name)
    end
end, false, { help = 'List your properties' })
```

A property table contains among others `Name`, `setName`, `Price`, `Interior`, `Entrance`, `Owner`, `Owned`, `Locked`, `Keys`, `garage`, `cctv`, `furniture` and `positions`. Treat it as read-only. Changes from other resources are not saved.

<Callout type="warn" title="Edge cases">
`GetPropertyKeys` and `DoesPlayerHaveKeys` index `Properties[propertyId]` without a nil check, so an invalid ID throws an error. Validate the ID first.
</Callout>

### Client [#client]

`exports.esx_property:GetProperties()` returns the property data the server synced to this player. For properties without access, the server strips owner, keys, furniture and garage data.

## Events and callbacks [#events-and-callbacks]

All interaction (buy, sell, lock, keys, furniture, garage, CCTV, raids, admin edits) goes through server callbacks named `esx_property:<action>`, for example `esx_property:buyProperty`, `esx_property:toggleLock`, `esx_property:GiveKey` and `esx_property:StoreVehicle`. Every callback is wrapped by a rate limiter, refuses to run while storage is not ready, validates property IDs and checks ownership, keys, admin groups or real estate grade on the server.

The client-triggerable net events are `esx_property:enter`, `esx_property:leave`, `esx_property:SetVehicleOut` and `esx_property:server:createProperty`. They are rate limited too. `enter` checks access and distance to the entrance, and `createProperty` requires admin or the `CreateProperty` grade and validates every field.

Client events sent by the server include `esx_property:syncProperties`, `esx_property:syncProperty`, `esx_property:receivePropertyOffer`, `esx_property:giveKeyAccess`, `esx_property:RemoveKey` and the furniture events (`syncFurniture`, `loadFurniture`, `placeFurniture`, `removeFurniture`, `editFurniture`).

## Common issues [#common-issues]

- **"properties.json is missing or empty; SQL migration was not completed".** The first start needs the shipped `properties.json`. Restore it, or insert at least one row and the migration marker manually if you start from scratch.
- **"SQL property IDs must be contiguous".** IDs in `esx_properties` must be 1..n without gaps. Do not delete rows by hand. Use the admin menu.
- **Storage marker does nothing.** Storage needs ox_inventory.
- **Wardrobe is empty.** Install esx_datastore and save outfits through [esx_clotheshop](/docs/addons/esx_clotheshop).
- **Vehicles stay "out" in your garage script.** Adjust `Config.Garage.MySQLquery` to your schema.
- **Shell interiors are black or missing.** Enable `Config.Shells` and install the shell pack the interiors refer to.

## Related [#related]

- [esx_society](/docs/addons/esx_society)
- [esx_datastore](/docs/addons/esx_datastore)
- [esx_garage](/docs/addons/esx_garage)
- [ox_inventory with ESX](/docs/guides/ox-inventory)
- [Admin permissions](/docs/guides/admin-permissions)
