QBESXStandalone
Agency-Housing Configuration
Database driven housing system for FiveM.
v1.1.0Paid5 Pages

01Configuration
Quick Installation · config.lua · Agency-Housing
--[[
╔══════════════════════════════════════════════════════════════════════╗
║ AGENCY HOUSING - CONFIGURATION ║
║ (c) Agency Scripts ║
╚══════════════════════════════════════════════════════════════════════╝
CONTENTS
1. Core Framework, language, database, debug, permissions,
standalone
2. Controls Keys, commands, interaction
3. Appearance Accent colours, door marker, map blips, sounds
4. Economy Buying, renting, selling to players, market
5. Creating Houses Interiors & shells, presets, Quick Place, placeable
houses, apartment buildings, property border, preview
6. Doors & Access MLO doors, doorbell, instances
7. Furnishing Build mode, gizmo, furniture catalogue
8. Upgrades & Cameras Camera module, live feed, door camera
9. Storage & Wardrobe Stash and outfits inside the house
10. Garage House garages for Agency-Garage, jg, qb or your own
11. Apps Phone & Pad app, Agency Companion app
Server owners only need to touch this file. Texts players see live in
locales/ (copy _template.lua for a new language).
]]
Config = {}
--==============================================================================
-- 1 · CORE
--==============================================================================
-- ── Framework ────────────────────────────────────────────────────────────────
-- Which framework the script works with.
--
-- 'auto' Recommended. Finds QBCore (qb-core), QBox (qbx_core) or ESX
-- (es_extended) by itself and sets everything up for it. When
-- none of the three is installed it runs standalone. A core
-- that starts late is picked up the moment it starts.
-- 'qbcore' QBCore or QBox only, never anything else.
-- 'esx' ESX only.
-- 'standalone' No framework, even if one is installed. See Standalone below
-- for what that means and what it cannot do.
--
-- The server console says on every start which one is active.
Config.Framework = 'auto'
-- ── Language & database ──────────────────────────────────────────────────────
-- Language file to use. See locales/ — copy _template.lua to add your own.
Config.Locale = 'en'
-- Database table name. The tables are created and updated on start, so no SQL
-- import is needed; sql/install.sql does the same by hand. If you change this
-- name, change it in sql/install.sql too.
Config.TableName = 'agency_houses'
-- ── Debug ────────────────────────────────────────────────────────────────────
-- Prints extra diagnostics to the server and client console. Meant for
-- support cases, keep false on a live server.
Config.Debug = false
-- ── Notifications ────────────────────────────────────────────────────────────
-- Heading shown on notifications. Without this Agency-Notify falls back to its
-- own default ("SYSTEM ALERT").
Config.NotifyTitle = 'Housing'
-- ── Action limits ────────────────────────────────────────────────────────────
-- Seconds a player has to wait between two actions that change something —
-- locking a door, handing over a key, saving a layout. Zero switches it off,
-- which is the default: it was a single throttle shared by every action, so two
-- unrelated clicks in a row swallowed the second one.
--
-- This is a throttle against event flooding, not a safety mechanism. Buying and
-- selling are protected separately by a per-house lock held across the database
-- write, and that does not depend on this value. Raise it only if you have a
-- reason to slow players down.
Config.ActionCooldown = 0.0
-- Wait per action and player, in seconds, for renting, selling, listing,
-- auto lock and the other actions in the market and the owner panel.
--
-- nil every action keeps its own short wait (0.4 to 2 seconds).
-- 0.0 no wait. A button reacts at once, even when pressed twice in a row.
--
-- Like ActionCooldown this only slows down event flooding. A double click can
-- never buy a house twice: a lock per house guards every purchase, whatever
-- this value is.
Config.ActionThrottle = 0.0
-- ── Permissions ──────────────────────────────────────────────────────────────
-- Who may open the house creator UI (/acreatehouse).
-- Checked in this order: FiveM ACE ("agency.housing" / "group.admin") -> framework group.
-- Standalone there are no groups: ACE, or Config.Standalone.admins below.
Config.AdminGroups = { 'admin', 'superadmin', 'god' }
-- ── Standalone ───────────────────────────────────────────────────────────────
-- Active with Config.Framework = 'standalone', or on 'auto' when no framework
-- is installed. The console then prints "Running standalone".
--
-- WORKS STANDALONE, NOTHING TO SET UP
-- Buying and selling, keys, locking, the doorbell, build mode and furniture,
-- upgrades, cameras, MLO doors, property borders, map blips, the rejoin
-- restore, the Agency Companion app and all exports. Everything is saved in
-- the database exactly as on a framework server.
-- Players are told apart by their Rockstar licence (license:...), their name
-- is their FiveM name. Messages go to Agency-Notify when it runs, otherwise
-- to the GTA notification feed.
-- Wardrobe: the built-in one (Config.Wardrobe.builtin) saves and puts back
-- outfits without any clothing script.
-- House garages: through Agency-Garage, which runs standalone as well
-- (jg-advancedgarages and qb-garages need their framework).
--
-- WORKS STANDALONE ONCE YOU FILL SOMETHING IN
-- Money: the three hooks below. Left empty, houses, upgrades and cameras are
-- free and selling pays nothing back.
-- Admins: ACE (add_ace group.admin agency.housing allow), or licences in
-- `admins` below.
-- Storage: needs an inventory resource. ox_inventory, qb-inventory,
-- qs-inventory, ps-inventory, lj-inventory and jpr-inventory are found by
-- themselves; any other one through Config.Stash.open.
--
-- DOES NOT WORK STANDALONE
-- Storage without an inventory resource. There are no items without one, so
-- the storage point is not offered at all rather than opening nothing.
-- Framework admin groups (Config.AdminGroups): there are no groups.
-- Characters: one licence is one player. Several characters on one account
-- share the same houses, keys and outfits.
-- Framework money accounts ('bank' / 'cash'): only what your hooks do with
-- Config.Account.
Config.Standalone = {
-- Licences that count as admin, for servers that do not hand out ACE.
-- Copy them from txAdmin (player > IDs). Example:
-- admins = { 'license:1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d' },
admins = {},
-- Money. Server side only. `account` is Config.Account, `amount` a number
-- above 0. removeMoney and addMoney must return true only when the money
-- actually moved; anything else, an error included, refuses the purchase
-- or the sale. Left as nil, nothing costs anything. Prices still show in
-- the menus, so set them to 0 in the creator if that reads wrong.
getMoney = nil, -- function(source, account) return balance end
removeMoney = nil, -- function(source, account, amount) return true end
addMoney = nil, -- function(source, account, amount) return true end
}
--==============================================================================
-- 2 · CONTROLS
--==============================================================================
-- ── Menu & keys ──────────────────────────────────────────────────────────────
-- Default key for the housing menu. Players can rebind it themselves under
-- Settings > Key Bindings > FiveM, so this is only the starting value.
Config.OpenKey = 'F7'
-- Command behind the hotkey. Must stay registered — RegisterKeyMapping needs it.
Config.MenuCommand = 'ahousing'
-- The old chat commands. The hotkey menu covers all of them; set this to false
-- if you do not want them cluttering the command list.
Config.RegisterCommands = true
-- How long the interface waits for the panel behind a menu tile before it
-- hands the mouse back, in milliseconds. Opening the creator or the property
-- list goes through the server, and the cursor is held over that gap so the
-- switch does not flicker. Raise it only if your server routinely answers
-- slower than this.
Config.PanelSwitchTimeout = 1500
Config.Commands = {
create = 'acreatehouse',
delete = 'adeletehouse',
list = 'ahouses',
}
-- Owner menu while standing inside your own house.
Config.ManageKey = 'F3'
Config.ManageCommand = 'ahousemenu'
-- ── Interaction ──────────────────────────────────────────────────────────────
Config.InteractKey = 38 -- E
Config.InteractDistance = 1.6
--==============================================================================
-- 3 · APPEARANCE
--==============================================================================
-- ── Accent colour ────────────────────────────────────────────────────────────
-- One colour runs through the whole script: the panels, the ground markers, the
-- property border and the outline around a piece of furniture. Players pick
-- theirs under Housing > Settings, and the choice is stored on their own machine
-- — it is a preference, not something the server decides for everyone.
--
-- `rgb` the colour itself.
-- `deep` the far end of the gradients on the buttons and the badge. Roughly the
-- same hue two steps darker; it is listed rather than calculated so a
-- server can tune it by eye.
-- `ink` text drawn ON the colour. A light accent needs dark ink and the other
-- way round, which is the one thing that cannot be derived safely.
--
-- Add your own by appending an entry. The swatch row in Settings is built from
-- this list, so nothing else has to be touched.
Config.DefaultAccent = 'gold'
Config.Accents = {
{ key = 'gold', label = 'Gold', rgb = { 226, 182, 73 }, deep = { 168, 128, 31 }, ink = { 23, 18, 10 } },
{ key = 'cyan', label = 'Cyan', rgb = { 0, 212, 255 }, deep = { 10, 127, 212 }, ink = { 0, 19, 29 } },
{ key = 'mint', label = 'Mint', rgb = { 53, 214, 166 }, deep = { 15, 118, 110 }, ink = { 4, 26, 21 } },
{ key = 'violet', label = 'Violet', rgb = { 168, 133, 247 }, deep = { 109, 74, 184 }, ink = { 20, 13, 33 } },
{ key = 'crimson', label = 'Crimson', rgb = { 255, 92, 92 }, deep = { 178, 42, 42 }, ink = { 32, 10, 10 } },
{ key = 'amber', label = 'Amber', rgb = { 255, 170, 0 }, deep = { 194, 112, 0 }, ink = { 32, 20, 0 } },
{ key = 'steel', label = 'Steel', rgb = { 214, 219, 226 }, deep = { 138, 146, 158 }, ink = { 22, 24, 29 } },
}
-- Whether the chosen colour also drives what is drawn in the world: the door
-- marker, the property border, the build fence, the storage marker and the
-- outline around a selected piece of furniture. Set to false to keep the
-- colours configured further down no matter what a player picks.
--
-- The garage keeps its own amber either way — that is what tells a parking spot
-- from a doorstep at a glance — and so does the wardrobe.
Config.AccentDrivesWorld = true
-- ── Door marker ──────────────────────────────────────────────────────────────
-- The marker on the ground at every front door.
Config.Marker = {
enabled = true,
type = 21,
size = { x = 0.28, y = 0.28, z = 0.22 },
color = { r = 226, g = 182, b = 73, a = 140 },
drawDistance = 12.0,
bobUpAndDown = true,
-- Entrances are recorded from the player's position, and a ped's origin
-- sits roughly 0.9 m above the ground it stands on. Without this correction
-- the marker floats at head height. Raise it towards 0 if your entrances
-- were recorded on stairs or a raised doorstep.
zOffset = -0.75,
-- Height of the "[E] House" text above the entrance.
textOffset = 0.35,
}
-- ── Map blips ────────────────────────────────────────────────────────────────
-- Colours a house can be given on the map, offered as swatches in the creator.
-- `id` is the GTA blip colour; `hex` is only what the swatch looks like in the
-- interface, so it can be tuned to match without touching the game value.
Config.BlipColors = {
{ id = 1, label = 'Red', hex = '#d6342b' },
{ id = 5, label = 'Yellow', hex = '#f2d43b' },
{ id = 17, label = 'Orange', hex = '#f08a20' },
{ id = 2, label = 'Green', hex = '#4caf50' },
{ id = 3, label = 'Blue', hex = '#29a3e0' },
{ id = 26, label = 'Purple', hex = '#9b59d0' },
{ id = 27, label = 'Pink', hex = '#e06fa5' },
{ id = 4, label = 'White', hex = '#f4f5f7' },
}
-- What a house may override on its own, and within which limits. A house that
-- overrides nothing follows the state colours below, which is still the normal
-- case — the overrides are for the handful of places that should stand out.
Config.BlipOverride = {
minScale = 0.40,
maxScale = 1.40,
step = 0.05,
}
-- Each state gets its own colour, scale and name suffix so a house changing
-- hands is actually visible on the map, not just technically different.
--
-- `shortRange = true` means the blip only appears on the minimap when you are
-- near it — that is the normal GTA behaviour and keeps the map clean. Set it to
-- false for a state you want pinned across the whole map at all times.
Config.Blips = {
enabled = true,
-- Sprite 374 is the house-with-a-dollar-sign icon: right for something on
-- the market, wrong once it belongs to someone. Sprite 40 is the plain
-- house without the price tag.
forSale = {
sprite = 374, color = 2, scale = 0.75, -- green, house + $
shortRange = true, suffix = ' — For sale',
},
owned = {
sprite = 40, color = 3, scale = 0.85, -- blue, plain house
shortRange = true, suffix = ' — Yours',
},
keyholder = {
sprite = 40, color = 26, scale = 0.70, -- purple, plain house
shortRange = true, suffix = ' — Key',
},
tenant = {
sprite = 40, color = 5, scale = 0.80, -- yellow, plain house
shortRange = true, suffix = ' — Rented',
},
forRent = {
sprite = 374, color = 5, scale = 0.70, -- yellow, house + $
shortRange = true, suffix = ' — For rent',
},
-- An owned house its owner put on the market. Shown to everyone, the same
-- way a house the server sells is.
listed = {
sprite = 374, color = 2, scale = 0.70, -- green, house + $
shortRange = true, suffix = ' — For sale',
},
other = {
sprite = 40, color = 4, scale = 0.60, -- white, plain house
shortRange = true, suffix = ' — Sold',
},
showOwnedByOthers = false, -- set true to draw the 'other' state above
}
-- ── Door sounds ──────────────────────────────────────────────────────────────
-- The door sounds are synthesised in the interface, not loaded from audio
-- files: files added to a resource do not reliably reach a client that already
-- has it cached, and a sample pack would have to be shipped and licensed.
-- Shape and pitch live in nui/script.js under DOOR.
Config.Sounds = {
enabled = true,
volume = 0.45, -- 0 to 1
-- How long the door takes to shut behind you, in milliseconds.
--
-- The slam used to fire the instant you pressed the exit marker, while you
-- were still standing in the room and the screen had not even begun to
-- fade. 400 ms puts it where the fade finishes and you are outside, which
-- is when a door actually falls shut. Raise it for a heavier door.
closeDelay = 400,
}
--==============================================================================
-- 4 · ECONOMY
--==============================================================================
-- ── Buying & selling ─────────────────────────────────────────────────────────
Config.Account = 'bank' -- 'bank' or 'cash' — account used to buy houses
Config.SellPercentage = 0.60 -- refund when a player sells their house
Config.MaxHousesPerPlayer = 2 -- 0 = unlimited
Config.MaxKeyholders = 8 -- how many people an owner may hand keys to
-- Standalone money: see Config.Standalone in 1 · CORE.
-- ── Renting ──────────────────────────────────────────────────────────────────
-- A house can be let. The landlord is its owner, or the server itself for a
-- house nobody owns (a motel room, a flat an admin put up for rent).
--
-- Rent is paid IN ADVANCE, one or more periods at a time. Nobody is ever
-- charged while offline, and nobody ends up in debt:
--
-- * Paid time runs out -> if the tenant is online and has autopay on, the
-- next period is taken from Config.Account and the lease simply goes on.
-- * Not online or not enough money -> the lease is "overdue" for graceHours.
-- The tenant keeps access and can pay from the panel (F3) at any time.
-- * Still unpaid after the grace time -> the lease ends by itself.
--
-- The landlord is paid the moment the tenant pays. Offline landlords receive
-- it the next time they join (see "payouts" in the README).
Config.Rent = {
enabled = true,
period = 7 * 24 * 60 * 60, -- one rent period, in real seconds (a week)
periodLabel = 'week', -- shown as "$2,500 / week"
maxPeriods = 4, -- how many periods may be paid ahead
graceHours = 24, -- how long an unpaid lease stays open
minPrice = 1,
maxPrice = 1000000,
maxPerPlayer = 1, -- leases one player may hold at once (0 = unlimited)
ownersMayLet = true, -- players may let their own houses
-- A house nobody owns is run by its tenant: they may furnish it, place
-- storage and wardrobe and hand out keys. When that lease ends the next
-- tenant should find an empty flat, so this resets furniture, fittings,
-- keys and (through Config.Stash.clear) the storage.
clearOnEnd = true,
}
-- ── Selling to a player ──────────────────────────────────────────────────────
-- The owner offers the house to a player standing next to them, for a price
-- they choose. The buyer gets a card with the house, the price and the seller
-- and decides. Everything is checked again the moment they accept: the seller
-- must still own the house, the buyer must still afford it and be allowed
-- another house. A price of 0 is a gift.
Config.Transfer = {
enabled = true,
distance = 8.0, -- how close the buyer has to stand
offerSeconds = 60, -- how long an offer stays open
maxPrice = 100000000, -- typo guard: nobody means $1,000,000,000
taxPercent = 0.0, -- share of the price that goes to nobody (0.05 = 5 %)
}
-- ── Market ───────────────────────────────────────────────────────────────────
-- F7 > Market. One place for everything that can be had: houses the server
-- sells, houses players have listed at their own price, houses for rent and
-- (with Placeable houses) buildings a player can put down themselves.
--
-- A listing is sold to whoever clicks Buy first. The seller is paid at once,
-- or the next time they join if they are offline.
Config.Market = {
enabled = true,
-- true: buy and rent straight from the market, wherever you are.
-- false: the market is a catalogue with waypoints; the deal is done at
-- the front door.
buyAnywhere = false,
feePercent = 0.0, -- share of a player sale kept by the market
minPrice = 1,
maxPrice = 100000000,
}
--==============================================================================
-- 5 · CREATING HOUSES
--==============================================================================
-- ── Interiors & shells ───────────────────────────────────────────────────────
-- `category` is the tab the entry shows under in the creator:
-- 'house' homes and apartments 'garage' garages
-- 'business' offices, clubs, bunkers... 'shell' shell props
-- Any other word gets a tab of its own. It is purely cosmetic -- `type` below
-- decides how the entry actually works. ('mlo', the old name of 'business', is
-- still understood: these are GTA's own interiors, not mapped MLOs.)
--
-- Two supported modes per shell entry:
--
-- type = 'interior' -> teleports into an existing GTA V interior. Works on ANY
-- server, no extra resource, no props to stream.
-- type = 'shell' -> spawns a shell prop under the map (needs a shells
-- resource such as qb-interiors / ox_shells that provides
-- the model). Left in as ready-to-use examples.
--
-- `interior` is the vector4 the player is teleported to (x, y, z, heading).
-- `exitOffset` is only used by type = 'shell'.
--
-- TIP: every coordinate below can be re-recorded live. Open /acreatehouse, stand
-- where you want the interior spawn to be and press "Use my position" — the
-- house is then stored with its own interior coords and this list is ignored
-- for that house. No config editing needed.
Config.ShellSpawn = vector3(1200.0, 4800.0, -100.0) -- base point for type='shell'
Config.Shells = {
-- ---------- VANILLA GTA V INTERIORS (no dependencies) ----------
['motel'] = {
label = 'Motel Room',
category = 'house',
type = 'interior',
price = 25000,
interior = vector4(152.11, -1004.36, -99.0, 340.0),
},
['apartment_low'] = {
label = 'Low End Apartment',
category = 'house',
type = 'interior',
price = 55000,
interior = vector4(265.97, -1007.44, -101.01, 0.0),
},
['apartment_mid'] = {
label = 'Mid End Apartment',
category = 'house',
type = 'interior',
price = 95000,
interior = vector4(346.77, -1012.85, -99.19, 180.0),
},
['apartment_high'] = {
label = 'High End Apartment',
category = 'house',
type = 'interior',
price = 180000,
interior = vector4(-786.87, 315.76, 217.64, 180.0),
},
['eclipse_towers'] = {
label = 'Eclipse Towers Penthouse',
category = 'house',
type = 'interior',
price = 420000,
interior = vector4(-774.01, 342.02, 196.69, 180.0),
},
['trailer'] = {
label = 'Trailer',
category = 'house',
type = 'interior',
price = 18000,
interior = vector4(1972.03, 3815.72, 33.43, 120.0),
},
['stilt_house'] = {
label = 'Stilt House (Vinewood Hills)',
category = 'house',
type = 'interior',
price = 650000,
interior = vector4(-174.29, 497.06, 137.67, 180.0),
},
['garage_small'] = {
label = 'Small Garage',
category = 'garage',
type = 'interior',
price = 40000,
interior = vector4(227.19, -991.42, -99.5, 90.0),
},
['garage_large'] = {
label = 'Large Garage',
category = 'garage',
type = 'interior',
price = 120000,
interior = vector4(174.06, -1002.66, -99.5, 90.0),
},
['office'] = {
label = 'Executive Office',
category = 'business',
type = 'interior',
price = 300000,
interior = vector4(-141.24, -620.83, 168.82, 270.0),
},
['clubhouse'] = {
label = 'Biker Clubhouse',
category = 'business',
type = 'interior',
price = 260000,
interior = vector4(1108.03, -3157.11, -37.51, 180.0),
},
['bunker'] = {
label = 'Bunker',
category = 'business',
type = 'interior',
price = 500000,
interior = vector4(894.53, -3245.86, -98.27, 0.0),
},
['hangar'] = {
label = 'Hangar',
category = 'business',
type = 'interior',
price = 380000,
interior = vector4(-1267.02, -3013.35, -49.5, 330.0),
},
['nightclub'] = {
label = 'Nightclub',
category = 'business',
type = 'interior',
price = 750000,
interior = vector4(-1605.53, -3012.7, -78.02, 180.0),
},
['arcade'] = {
label = 'Arcade',
category = 'business',
type = 'interior',
price = 340000,
interior = vector4(2721.5, -376.02, -48.5, 180.0),
},
-- ---------- SHELL PROPS (require a shells resource) ----------
['shell_small'] = {
label = 'Small Shell House',
category = 'shell',
type = 'shell',
model = 'shell_v16low',
price = 90000,
exitOffset = vector3(0.0, -3.5, 1.0),
},
['shell_medium'] = {
label = 'Medium Shell House',
category = 'shell',
type = 'shell',
model = 'shell_v16mid',
price = 170000,
exitOffset = vector3(1.4, -4.2, 1.1),
},
['shell_large'] = {
label = 'Large Shell Mansion',
category = 'shell',
type = 'shell',
model = 'shell_v16high',
price = 400000,
exitOffset = vector3(3.2, -6.1, 1.2),
},
}
-- Order the shells appear in the creator UI. Keys not listed are appended.
Config.ShellOrder = {
'trailer', 'motel', 'apartment_low', 'garage_small', 'apartment_mid',
'garage_large', 'apartment_high', 'clubhouse', 'office', 'arcade',
'hangar', 'eclipse_towers', 'bunker', 'stilt_house', 'nightclub',
'shell_small', 'shell_medium', 'shell_large',
}
-- ── Presets & Quick Place ────────────────────────────────────────────────────
-- A preset is a house without a place: interior, price, rent, border size
-- and blip. Pick one in the creator and the form fills itself; the border is
-- drawn as a rectangle around the front door, so a house is one click.
--
-- Quick Place (F7 > Quick Place, admins) goes one step further: choose a
-- preset, walk from door to door and press E at each — every press is a new
-- house, named after the street it stands on.
--
-- Admins can also save the creator's current setup as a preset in game; those
-- are stored in the database and listed after the ones below.
--
-- `zone` the border: `width` across the door, `depth` back into the plot,
-- `front` how far it reaches in front of the door (the front garden).
-- `rent` optional: `price` puts the house up for rent straight away.
Config.Presets = {
{ key = 'motel', label = 'Motel Room', shell = 'motel', price = 25000, rent = { price = 900 }, zone = { width = 8, depth = 8, front = 2 } },
{ key = 'starter', label = 'Starter Apartment', shell = 'apartment_low', price = 55000, rent = { price = 1800 }, zone = { width = 12, depth = 14, front = 3 } },
{ key = 'family', label = 'Family Home', shell = 'apartment_mid', price = 95000, zone = { width = 18, depth = 22, front = 6 } },
{ key = 'luxury', label = 'Luxury Apartment', shell = 'apartment_high', price = 180000, zone = { width = 20, depth = 24, front = 6 } },
{ key = 'villa', label = 'Hillside Villa', shell = 'stilt_house', price = 650000, zone = { width = 34, depth = 40, front = 10 } },
}
-- Border used by the creator's "Auto border" button when no preset is chosen.
Config.DefaultPresetZone = { width = 14, depth = 16, front = 4 }
-- ── Placeable houses ─────────────────────────────────────────────────────────
-- Buildings that are not part of the map: a portacabin, a container home, a
-- cabin. They are props, placed like furniture — a see-through copy follows
-- the camera, green where it may stand and red (with the reason) where it may
-- not. Confirming creates a complete house: the building, a front door, a
-- property border around it and the interior behind the door.
--
-- Admins place them from F7 > Market > Build (free, the house goes on sale).
-- Players may too, when `players` is true: they pay `price` and own it.
--
-- `model` any object, base game or streamed. Entries whose model does not
-- exist on the client are hidden, so a list shared between servers
-- never shows a building that cannot spawn.
-- `interior` a key from Config.Shells: what is behind the door.
-- `door` optional offset of the front door from the model's centre
-- (x = right, y = forward, z = up). Left out, the door sits in the
-- middle of the front face.
-- `margin` metres of garden added around the building for the border.
Config.Placeables = {
enabled = true,
players = false, -- players may build their own
maxPerPlayer = 1, -- player-built houses per player (0 = unlimited)
-- Where a building may not go. Checked while aiming and again by the server.
rules = {
minDistance = 20.0, -- metres to the next front door
noRoads = true, -- not on a road
noWater = true, -- not in water
maxSlope = 1.6, -- metres the ground may drop across the footprint
-- How far from the player a building may stand, in metres. Placing
-- is done from a camera in the air, so keep this well above the
-- camera height. The server allows reach + 8.
reach = 120.0,
-- Players building from the market aim with a camera from the air; it
-- stays within this many metres of them, and they are put back where
-- they stood afterwards (no free flight across the map).
camRange = 50.0,
},
-- How far the buildings are drawn. They are local props, spawned near the
-- player and removed again further out.
drawDistance = 180.0,
list = {
-- doorSide: which face of the model has its door ('front' if left out,
-- 'back', 'left', 'right'); doorShift moves the entrance along that
-- face, seen from outside: negative = left, positive = right (metres).
{ key = 'portacabin', label = 'Portacabin', model = 'prop_portacabin01', price = 45000, interior = 'motel', margin = 4.0, doorSide = 'back', doorShift = -1.6 },
{ key = 'container', label = 'Container Home', model = 'prop_container_05a', price = 30000, interior = 'trailer', margin = 4.0 },
{ key = 'container2', label = 'Container Loft', model = 'prop_container_01a', price = 38000, interior = 'apartment_low', margin = 4.0 },
},
}
-- ── Apartment buildings ──────────────────────────────────────────────────────
-- The creator can build several apartments behind one front door. Each is a
-- house of its own -- own interior instance, keys, furniture, lease -- that
-- shares entrance, ground and garage with the others. At the door, players
-- pick the apartment from a list.
Config.Apartments = {
maxUnits = 40, -- most apartments one building may have
}
-- ── Property border ──────────────────────────────────────────────────────────
-- The ground the house owns: the garden, the driveway, the yard, the rooms of
-- an MLO. Every house has one — the border is walked on foot in the creator,
-- corner by corner, and a house cannot be saved without it.
--
-- Walked rather than picked, because a plot is never round. An L shaped yard, a
-- driveway down one side, an MLO that has to be fenced along its own walls: a
-- circle either misses half of it or swallows the street.
--
-- The borders are handed to PolyZone (https://github.com/mkafrin/PolyZone),
-- which is a required resource — see the README. Every border on the server is
-- bundled into a single ComboZone, so the cost stays the same whether there are
-- five properties or five hundred.
Config.Zone = {
-- How often the player's position is tested against the borders, in ms.
-- 500 is PolyZone's own default and is plenty for walking pace.
checkInterval = 500,
-- Draws every border as a green PolyZone debug volume. Development only.
debugPoly = false,
poly = {
minPoints = 3, -- fewer than three corners enclose nothing
maxPoints = 24, -- the creator stops accepting corners here
-- Corners dropped closer together than this are ignored, so holding E
-- while walking does not fill the polygon with duplicates.
snap = 1.2,
-- No height setting. A property is the ground it stands on, all the
-- way up and all the way down — a balcony, a roof terrace or the top
-- floor of an MLO belongs to the plot as much as the lawn does.
},
-- The camera the border is marked with. It lifts off above the admin and
-- looks down, so the whole plot is in frame from the first second — walking
-- a boundary on foot means never seeing the shape you are making.
--
-- A line is cast from the camera onto the ground and a pole is drawn where
-- it lands. That spot, not the camera, is where the corner goes.
cam = {
height = 14.0, -- metres above the player when it starts
pitch = -55.0, -- how far it looks down to begin with
speed = 0.35, -- metres per frame
boost = 3.2, -- multiplier while holding Shift
slow = 0.30, -- multiplier while holding Alt
lookSens = 5.0, -- mouse sensitivity
reach = 600.0, -- how far the marker line is cast, in metres
pole = 2.6, -- height of the pole drawn at the marked spot
},
-- The fence drawn while a border is being marked in the creator. It is not
-- shown during play at all: a wall in the middle of a garden is in the way
-- of the house it belongs to, and from inside it stands in your window.
borderColor = { r = 226, g = 182, b = 73, a = 90 },
-- Other properties while a new one is set up: red, within otherDistance m.
otherColor = { r = 255, g = 73, b = 73, a = 110 },
otherDistance = 150.0,
-- A new border may not take ground that belongs to another house: more
-- than overlapTolerance m² shared is refused. true = allow it anyway.
allowOverlap = false,
overlapTolerance = 1.0,
borderHeight = 3.5, -- how tall that fence is drawn, in metres
}
-- ── 3D preview (creator "View in 3D") ────────────────────────────────────────
Config.Preview = {
distance = 3.2, -- starting camera distance in metres
minDist = 0.8, -- fully zoomed in
maxDist = 7.0, -- fully zoomed out (kept inside the room, see fovMax)
zoomStep = 0.45, -- metres per mouse wheel notch
zoomSmooth = 0.25, -- 0..1, how fast the camera eases to the new distance
orbitSpeed = 0.25, -- degrees per frame
height = 0.9, -- camera height above the interior spawn point
-- GTA interiors are only drawn while the camera is inside them, so pulling
-- the camera far enough back to "see it all" would just show the void.
-- Zooming out widens the lens instead: same viewpoint, much more in frame.
fovMin = 50.0, -- fully zoomed in
fovMax = 100.0, -- fully zoomed out
-- Free-fly mode (F). A camera has no body: it cannot fall through the map,
-- get stuck in geometry or take fall damage, which is why the preview flies
-- instead of teleporting the player.
freeSpeed = 0.10, -- metres per frame
freeBoost = 3.5, -- multiplier while holding Shift
freeSlow = 0.30, -- multiplier while holding Alt
lookSens = 6.0, -- mouse sensitivity
fadeMs = 150, -- blend between entries
}
--==============================================================================
-- 6 · DOORS & ACCESS
--==============================================================================
-- ── MLO doors ────────────────────────────────────────────────────────────────
-- An MLO is part of the map: you cannot be teleported into it, you walk through
-- its front door. So an MLO house is locked at that door rather than at a
-- marker. The admin records the doors in the creator by looking at them; every
-- client then registers them with the game's own door system, and the owner and
-- their keyholders can lock and unlock them on the spot.
Config.Doors = {
max = 8, -- doors per house; a double door is two of them
range = 1.6, -- how close you have to stand to work the lock
-- Gates and garage doors (wider than 2.4 m) are also worked from the car,
-- from up to this many metres away. Ordinary doors only on foot.
vehicleRange = 6.0,
-- How far the creator's aim reaches when recording a door.
reach = 8.0,
-- The dome sitting on a door, red while locked and green while open, the
-- same reading Agency Doorlock uses. Off: up close it is a large coloured
-- blob across the door itself, and the [E] card already says everything it
-- said. Set enabled = true to bring it back — a smaller `size` is what
-- makes it bearable at arm's length.
--
-- Shown to everyone when on, not only to the owner: that a door can be
-- locked at all is public information, whether you may work it or not.
marker = {
enabled = false,
type = 28, -- dome
distance = 12.0, -- metres
size = 0.18,
alpha = 110,
bob = false,
onlyWhenLocked = false,
},
colors = {
locked = { r = 255, g = 72, b = 72 },
unlocked = { r = 53, g = 214, b = 166 },
},
-- The [E] card at the bottom of the screen. Set to false for the plain 3D
-- text this script drew before.
prompt = true,
-- The gesture played on whoever works the lock: a short forward reach, as
-- if for a handle or a key fob. The same one Agency Doorlock uses, so a
-- player sees one movement for locking a door anywhere on the server.
--
-- Flag 48 in the code below is upper body plus secondary, which is what
-- lets the animation play without stopping the player walking.
animation = {
enabled = true,
dict = 'anim@heists@keycard@',
name = 'exit',
duration = 1100, -- ms
},
-- How far from the house a lock may be worked. Checked by the server, so a
-- modified client cannot unlock a door from across the map. Inside the
-- house, on its property or this close to a door or the entrance counts.
serverRange = 25.0,
-- Two presses of E in a row used to lock and unlock again at once. Any
-- toggle within this many seconds of the last one is ignored. 0.0 lets
-- the lock react at once; raise it if two keyholders at the same door
-- keep locking and unlocking it against each other.
debounce = 0.0,
-- Owners (and the tenant of a house nobody owns) can have their doors lock
-- themselves again after being opened. The choices offered in the panel,
-- in seconds; 0 is "off".
autoLock = { 0, 30, 60, 300 },
-- Someone without a key standing at a locked MLO door gets "[E] Ring" on
-- the card instead of a dead padlock. The bell then works like the one at
-- the front door.
ringAtDoor = true,
}
-- ── Doorbell ─────────────────────────────────────────────────────────────────
Config.Doorbell = {
enabled = true,
cooldown = 12, -- seconds between rings, per house, to stop bell spam
range = 12.0, -- how far from the entrance the ring is still heard
waitTime = 90, -- seconds a caller stays listed in the owner's panel
-- Played for whoever is inside. Any frontend sound pair works.
sound = { name = 'TIMER_STOP', set = 'HUD_MINI_GAME_SOUNDSET' },
}
-- ── Instances ────────────────────────────────────────────────────────────────
-- Every house gets its own routing bucket while someone is inside, so two
-- houses that use the same interior never show their players to each other.
-- The buckets used are bucketOffset + house id. Change the offset only if
-- another script already uses buckets in that range.
Config.Instances = {
enabled = true,
bucketOffset = 7000,
}
--==============================================================================
-- 7 · FURNISHING
--==============================================================================
-- ── Build mode ───────────────────────────────────────────────────────────────
Config.MaxFurniture = 60 -- objects per house
-- Selling a house (to the bank, on the market, to a player) removes its
-- furniture, so the next owner moves into an empty house. false = the
-- furniture stays with the building.
Config.ClearFurnitureOnSale = true
Config.FurnitureReach = 6.0 -- how far in front of the camera you can place
-- The build camera is fenced in, otherwise you fly straight out of the
-- apartment and across the map. Which fence applies depends on where you are
-- furnishing from:
--
-- inside a preset interior or a shell -- there is no plot around you, the
-- room sits under the map, so the camera is held near the spawn point.
-- on your own property (an MLO) -- the property border itself is the fence.
-- You may furnish the whole plot: the garden, the driveway, every floor.
Config.BuildBounds = {
radius = 16.0, -- metres from the interior spawn point
below = 4.0, -- how far you may drop below it
above = 7.0, -- and rise above it
-- Vertical range when the border is the fence. The plot itself has no
-- ceiling, but a build camera needs one or it ends up in the stratosphere.
zoneBelow = 8.0,
zoneAbove = 30.0,
-- The border drawn while furnishing a plot. Nothing draws it during normal
-- play; in build mode it is the edge of what you may place on, and a fence
-- you cannot see is one you keep bumping into. Taller than the creator's,
-- because here you are looking down at it from a camera.
fenceHeight = 4.0,
fenceColor = { r = 226, g = 182, b = 73, a = 55 },
}
-- ── Gizmo (the axes on a selected piece) ─────────────────────────────────────
-- Click a piece in build mode and three axes appear on it. Drag one and only
-- that direction changes; nothing slides sideways, nothing tilts by accident.
-- G switches to moving, R to turning.
Config.Gizmo = {
enabled = true,
-- Degrees a piece on the cursor turns per wheel step (free placing). The
-- wheel lands on multiples of this, so pieces stand square; Alt+wheel
-- turns by wheelFine for fine-tuning.
wheelStep = 15.0,
wheelFine = 1.0,
-- How large the axes are on screen. They are drawn in metres but scaled
-- with the camera distance, so this is roughly "fraction of the view".
screenSize = 0.14,
-- How close the cursor has to be to an axis to grab it, in screen units
-- (0..1 across the window). 0.02 is about 20 px on a 1080p screen.
pickRadius = 0.022,
-- The axes start this far above the piece's origin, or they would come out
-- of the floor for anything standing on the ground.
lift = 0.05,
-- The grid every drag snaps to. Either at 0 turns snapping off.
moveSnap = 0.05, -- metres
rotateSnap = 5.0, -- degrees
-- How many degrees a full sweep of the handle across the screen turns the
-- piece. Lower is finer.
rotateSpan = 360.0,
}
-- ── Furniture catalogue ──────────────────────────────────────────────────────
-- Catalogue for the furnishing mode. All GTA V base game props, so nothing has
-- to be streamed. `cat` only groups them in the UI.
--
-- Entries whose model does not exist in the running game are dropped on the
-- client before the catalogue is shown (IsModelValid). That filter is what
-- makes the generated families below safe: GTA prop names follow patterns, so
-- the ranges are expanded blindly and whatever the game does not have simply
-- never appears. Adding a range costs nothing and misses nothing.
--
-- `variants` = number of texture variations (SET_OBJECT_TEXTURE_VARIATION).
-- Most props have none, so the colour picker stays hidden unless set here.
-- Use /housevariants on a placed object to find out.
--
-- `image` = optional path to a real thumbnail, e.g. 'nui/img/couch.png'.
-- Otherwise the card shows the picture taken by /housethumbs, or a drawn icon.
Config.Furniture = {}
--- Add one prop by name.
local seen = {} -- a model listed twice shows up once
local function add(cat, label, model, extra)
if seen[model] then return end
seen[model] = true
local entry = { cat = cat, label = label, model = model }
if extra then
for k, v in pairs(extra) do entry[k] = v end
end
Config.Furniture[#Config.Furniture + 1] = entry
end
--- Expand a numbered family, optionally with letter variants.
--- family('seating', 'Chair', 'prop_chair_%02d%s', 1, 12, { '', 'a', 'b', 'c' })
--- produces prop_chair_01, prop_chair_01a, prop_chair_01b … and the client
--- throws away the ones this copy of GTA does not have.
local function family(cat, label, pattern, from, to, letters)
letters = letters or { '' }
for n = from, to do
for _, letter in ipairs(letters) do
local model = pattern:format(n, letter)
local name = ('%s %d%s'):format(label, n, letter ~= '' and letter:upper() or '')
add(cat, name, model)
end
end
end
local AB = { '', 'a', 'b' }
local ABC = { '', 'a', 'b', 'c' }
-- ---------------- SEATING ----------------
family('seating', 'Chair', 'prop_chair_%02d%s', 1, 12, ABC)
family('seating', 'Office Chair', 'prop_off_chair_%02d%s', 1, 8, AB)
family('seating', 'Bench', 'prop_bench_%02d%s', 1, 12, AB)
family('seating', 'Bar Stool', 'prop_bar_stool_%02d%s', 1, 3, AB)
family('seating', 'Beach Chair', 'prop_beach_lounger_%02d%s',1, 3, AB)
family('seating', 'Sofa', 'prop_rub_couch%02d%s', 1, 4, AB)
add('seating', 'Living Room Sofa', 'v_club_officesofa')
add('seating', 'Lounge Sofa', 'v_res_mbsofa')
add('seating', 'Corner Sofa', 'v_res_d_sofa')
add('seating', 'Trailer Sofa', 'v_res_tre_sofa')
add('seating', 'Armchair', 'v_res_tre_chair')
add('seating', 'Leather Armchair', 'v_res_m_armchair')
add('seating', 'Beanbag', 'prop_beanbag_01')
add('seating', 'Wheelchair', 'prop_wheelchair_01')
-- ---------------- TABLES ----------------
family('tables', 'Table', 'prop_table_%02d%s', 1, 12, ABC)
family('tables', 'Desk', 'prop_off_desk_%02d%s', 1, 6, AB)
family('tables', 'Worktop', 'prop_tool_bench%02d%s', 1, 5, AB)
add('tables', 'Glass Table', 'v_res_tre_table')
add('tables', 'Coffee Table', 'v_res_mcoftable')
add('tables', 'Poker Table', 'prop_poker_tabl_01')
add('tables', 'Pool Table', 'prop_pooltable_02')
add('tables', 'Patio Table', 'prop_patio_table_01')
-- ---------------- BEDROOM ----------------
add('bedroom', 'Double Bed', 'v_res_fh_bed')
add('bedroom', 'Bed', 'v_res_mbed')
add('bedroom', 'Single Bed', 'v_res_tre_bed')
add('bedroom', 'Bunk Bed', 'v_res_bunkbed')
add('bedroom', 'Mattress', 'prop_rub_matress_01')
family('bedroom', 'Mattress', 'prop_rub_matress_%02d%s', 1, 4, AB)
add('bedroom', 'Wardrobe', 'v_res_fh_wardrobe')
add('bedroom', 'Closet', 'v_res_mwardrobe')
add('bedroom', 'Dresser', 'v_res_tre_drawers')
add('bedroom', 'Chest of Drawers', 'v_res_fh_drawer')
add('bedroom', 'Nightstand', 'v_res_m_bedsidetbl')
family('bedroom', 'Mirror', 'prop_wall_mirror_%02d%s', 1, 3, AB)
-- ---------------- KITCHEN ----------------
family('kitchen', 'Fridge', 'prop_fridge_%02d%s', 1, 5, AB)
family('kitchen', 'Microwave', 'prop_micro_%02d%s', 1, 3, AB)
family('kitchen', 'Cooker', 'prop_cooker_%02d%s', 1, 6, AB)
family('kitchen', 'Sink', 'prop_sink_%02d%s', 1, 8, AB)
family('kitchen', 'Bin', 'prop_bin_%02d%s', 1, 12, AB)
add('kitchen', 'Bar Fridge', 'prop_bar_fridge_01')
add('kitchen', 'Kitchen Counter', 'v_res_kitchen_unit')
add('kitchen', 'Coffee Machine', 'prop_coffee_mac_01')
add('kitchen', 'Toaster', 'prop_toaster_01')
add('kitchen', 'Kettle', 'prop_kettle_01')
add('kitchen', 'Water Cooler', 'prop_watercooler')
add('kitchen', 'Bar Counter', 'prop_beer_bar_01')
add('kitchen', 'Wine Rack', 'prop_wine_rack_01')
-- ---------------- ELECTRONICS ----------------
family('tech', 'TV', 'prop_tv_flat_%02d%s', 1, 5, AB)
family('tech', 'Monitor', 'prop_monitor_%02d%s', 1, 4, AB)
family('tech', 'PC', 'prop_pc_%02d%s', 1, 4, AB)
family('tech', 'Laptop', 'prop_laptop_%02d%s', 1, 4, AB)
family('tech', 'Keyboard', 'prop_keyboard_%02d%s', 1, 3, AB)
family('tech', 'Printer', 'prop_printer_%02d%s', 1, 3, AB)
family('tech', 'Speaker', 'prop_speaker_%02d%s', 1, 9, AB)
family('tech', 'Arcade Machine', 'prop_arcade_%02d%s', 1, 4, AB)
add('tech', 'TV Stand', 'v_res_tre_tvstand')
add('tech', 'Stereo', 'prop_boombox_01')
add('tech', 'Telephone', 'prop_phone_ing')
add('tech', 'Dartboard', 'prop_dartboard_01')
-- ---------------- LIGHTING ----------------
family('light', 'Floor Lamp', 'prop_stand_lamp_%02d%s', 1, 4, AB)
family('light', 'Table Lamp', 'prop_lamp_%02d%s', 1, 8, AB)
family('light', 'Desk Lamp', 'prop_desk_lamp_%02d%s', 1, 3, AB)
family('light', 'Ceiling Light', 'prop_light_ceiling_%02d%s',1, 5, AB)
family('light', 'Wall Light', 'prop_wall_light_%02d%s', 1, 15, AB)
family('light', 'Candle', 'prop_candle_%02d%s', 1, 4, AB)
add('light', 'Neon Sign', 'prop_neon_sign_01')
-- ---------------- DECOR ----------------
family('decor', 'Rug', 'prop_rug_%02d%s', 1, 6, AB)
family('decor', 'Shelf', 'prop_shelves_%02d%s', 1, 6, AB)
family('decor', 'Books', 'prop_books_%02d%s', 1, 6, AB)
family('decor', 'Plant', 'prop_plant_%02d%s', 1, 4, ABC)
family('decor', 'Indoor Plant', 'prop_plant_int_%02d%s', 1, 6, ABC)
family('decor', 'Fern', 'prop_plant_fern_%02d%s', 1, 3, ABC)
family('decor', 'Potted Plant', 'prop_pot_plant_%02d%s', 1, 8, ABC)
family('decor', 'Cactus', 'prop_cactus_%02d%s', 1, 4, AB)
family('decor', 'Vase', 'prop_vase_%02d%s', 1, 6, AB)
family('decor', 'Clock', 'prop_clock_%02d%s', 1, 4, AB)
family('decor', 'Boxes', 'prop_boxpile_%02d%s', 1, 8, ABC)
family('decor', 'Crate', 'prop_box_wood%02d%s', 1, 8, AB)
family('decor', 'Painting', 'prop_painting_%02d%s', 1, 6, AB)
add('decor', 'Bookcase', 'v_res_tre_bookcase')
add('decor', 'Curtains', 'v_res_curtains')
add('decor', 'Safe', 'p_v_43_safe_s')
add('decor', 'Gun Cabinet', 'prop_gun_case_01')
add('decor', 'Dumbbell', 'prop_dumbbell_01')
add('decor', 'Punching Bag', 'prop_boxing_bag_01')
add('decor', 'Weight Bench', 'prop_gym_bench_01')
add('decor', 'Fireplace', 'prop_fireplace_01')
add('decor', 'Aquarium', 'prop_fish_tank_01')
add('decor', 'Statue', 'prop_statue_01')
add('decor', 'Globe', 'prop_globe_01')
-- ---------------- MORE FURNITURE ----------------
-- The furniture of GTA Online's apartments, offices, clubhouses and yachts
-- (apa_mp_h_*, ex_*, bkr_*, ba_*, ...), plus bathroom, garden, office and
-- fitness pieces. Anything this copy of GTA does not know is dropped when
-- build mode opens, so a missing DLC only shortens the list.
-- ---------------- SEATING ----------------
family('seating', 'Corner Sofa', 'apa_mp_h_stn_sofacorn_%02d%s', 1, 10)
family('seating', 'Designer Armchair', 'apa_mp_h_stn_chairarm_%02d%s', 1, 26)
family('seating', 'Strip Chair', 'apa_mp_h_stn_chairstrip_%02d%s', 1, 8)
family('seating', 'Dining Chair', 'apa_mp_h_din_chair_%02d%s', 1, 12)
family('seating', 'Couch', 'prop_couch_%02d%s', 1, 4)
add('seating', 'Two-Seater Sofa', 'apa_mp_h_stn_sofa2seat_02')
add('seating', 'Daybed', 'apa_mp_h_stn_sofa_daybed_01')
add('seating', 'Daybed Long', 'apa_mp_h_stn_sofa_daybed_02')
add('seating', 'Designer Stool', 'apa_mp_h_stn_chairstool_12')
add('seating', 'Footstool', 'apa_mp_h_stn_foot_stool_01')
add('seating', 'Footstool Round', 'apa_mp_h_stn_foot_stool_02')
add('seating', 'Dining Stool', 'apa_mp_h_din_stool_04')
add('seating', 'Yacht Sofa', 'apa_mp_h_yacht_sofa_01')
add('seating', 'Yacht Sofa Long', 'apa_mp_h_yacht_sofa_02')
add('seating', 'Yacht Armchair', 'apa_mp_h_yacht_armchair_01')
add('seating', 'Yacht Armchair 2', 'apa_mp_h_yacht_armchair_03')
add('seating', 'Yacht Armchair 3', 'apa_mp_h_yacht_armchair_04')
add('seating', 'Yacht Stool', 'apa_mp_h_yacht_stool_01')
add('seating', 'Large Couch', 'prop_couch_lg_05')
add('seating', 'Large Couch 2', 'prop_couch_lg_06')
add('seating', 'Large Couch 3', 'prop_couch_lg_07')
add('seating', 'Large Couch 4', 'prop_couch_lg_08')
add('seating', 'Small Couch', 'prop_couch_sm_06')
add('seating', 'Small Couch 2', 'prop_couch_sm_07')
add('seating', 'Michael\'s Sofa', 'v_ilev_m_sofa')
add('seating', 'Franklin\'s Sofa', 'v_res_fh_sofa')
add('seating', 'Modern Sofa', 'v_res_mp_sofa')
add('seating', 'Studio Sofa', 'v_res_tt_sofa')
add('seating', 'Small Leather Sofa', 'v_res_m_h_sofa_sml')
add('seating', 'Padded Armchair', 'v_res_d_armchair')
add('seating', 'Highchair', 'v_res_d_highchair')
add('seating', 'Kitchen Chair', 'v_res_fa_chair01')
add('seating', 'Kitchen Chair 2', 'v_res_fa_chair02')
add('seating', 'Club Armchair', 'v_res_jarmchair')
add('seating', 'Strip Chair Red', 'v_res_mp_stripchair')
add('seating', 'Bedroom Chair', 'v_res_mbchair')
add('seating', 'Office Sofa', 'ex_mp_h_off_sofa_01')
add('seating', 'Office Sofa 2', 'ex_mp_h_off_sofa_02')
add('seating', 'Office Sofa 3', 'ex_mp_h_off_sofa_03')
add('seating', 'Office Easy Chair','ex_mp_h_off_easychair_01')
add('seating', 'Clubhouse Armchair', 'bkr_prop_clubhouse_armchair_01a')
add('seating', 'Clubhouse Chair', 'bkr_prop_clubhouse_chair_01')
add('seating', 'Biker Chair', 'bkr_prop_biker_chair_01')
add('seating', 'Club Chair', 'ba_prop_battle_club_chair_01')
add('seating', 'Club Chair 2', 'ba_prop_battle_club_chair_02')
add('seating', 'Club Chair 3', 'ba_prop_battle_club_chair_03')
add('seating', 'Garage Sofa', 'imp_prop_impexp_sofa_01a')
add('seating', 'Director Chair', 'prop_direct_chair_01')
add('seating', 'Director Chair 2', 'prop_direct_chair_02')
add('seating', 'Deck Chair', 'prop_old_deck_chair')
add('seating', 'Farm Chair', 'prop_ld_farm_chair01')
add('seating', 'Folding Chair', 'prop_cs_folding_chair_01')
-- ---------------- TABLES ----------------
family('tables', 'Large Side Table', 'apa_mp_h_tab_sidelrg_%02d%s', 1, 7)
family('tables', 'Small Side Table', 'apa_mp_h_tab_sidesml_%02d%s', 1, 2)
family('tables', 'Designer Coffee Table', 'apa_mp_h_tab_coffee_%02d%s', 5, 8)
family('tables', 'Dining Table', 'apa_mp_h_din_table_%02d%s', 1, 11)
family('tables', 'Folding Table', 'prop_table_para_comb_%02d%s', 1, 5)
family('tables', 'Picnic Table', 'prop_picnictable_%02d%s', 1, 2)
add('tables', 'Yacht Coffee Table','apa_mp_h_yacht_coffee_table_01')
add('tables', 'Yacht Coffee Table 2', 'apa_mp_h_yacht_coffee_table_02')
add('tables', 'Yacht Side Table', 'apa_mp_h_yacht_side_table_01')
add('tables', 'Yacht Side Table 2','apa_mp_h_yacht_side_table_02')
add('tables', 'Dinner Table', 'v_res_m_dinetble')
add('tables', 'Eames Table', 'v_res_fh_dineeamesa')
add('tables', 'Wooden Coffee Table','v_res_d_coffeetable')
add('tables', 'Display Coffee Table','v_res_fh_coftbldisp')
add('tables', 'Dark Coffee Table', 'v_res_fh_coftableb')
add('tables', 'Lounge Coffee Table','v_res_j_coffeetable')
add('tables', 'Office Desk', 'prop_office_desk_01')
add('tables', 'Market Table', 'prop_ven_market_table1')
add('tables', 'Table Tennis', 'prop_table_tennis')
add('tables', 'Dressing Table', 'v_res_d_dressingtable')
-- ---------------- BEDROOM ----------------
family('bedroom', 'Designer Bed', 'apa_mp_h_bed_double_%02d%s', 8, 9)
family('bedroom', 'Sideboard', 'apa_mp_h_str_sideboardl_%02d%s', 6, 14)
family('bedroom', 'Small Sideboard','apa_mp_h_str_sideboards_%02d%s', 1, 2)
family('bedroom', 'Medium Sideboard','apa_mp_h_str_sideboardm_%02d%s', 2, 3)
add('bedroom', 'Wide Bed', 'apa_mp_h_bed_wide_05')
add('bedroom', 'Bed with Tables', 'apa_mp_h_bed_with_table_02')
add('bedroom', 'Yacht Bed', 'apa_mp_h_yacht_bed_01')
add('bedroom', 'Yacht Bed 2', 'apa_mp_h_yacht_bed_02')
add('bedroom', 'Floor Shelf', 'apa_mp_h_str_shelffloorm_02')
add('bedroom', 'Wall Shelf', 'apa_mp_h_str_shelfwallm_01')
add('bedroom', 'Son\'s Bed', 'v_res_msonbed')
add('bedroom', 'Double Bed Dark', 'v_res_d_bed')
add('bedroom', 'Bedside Table', 'v_res_tre_bedsidetable')
add('bedroom', 'Bedside Table 2', 'v_res_mbbedtable')
add('bedroom', 'Studio Bed', 'v_res_tt_bed')
add('bedroom', 'Dresser Tall', 'v_res_mbdresser')
add('bedroom', 'Dresser Low', 'v_res_d_dresser')
add('bedroom', 'Dark Sideboard', 'v_res_fh_sidebrddark')
add('bedroom', 'Long Sideboard', 'v_res_fh_sidebrdlngb')
add('bedroom', 'Cabinet', 'v_res_cabinet')
add('bedroom', 'Trailer Wardrobe', 'v_res_tre_wardrobe')
add('bedroom', 'Storage Unit', 'v_res_tre_storageunit')
add('bedroom', 'Storage Box', 'v_res_tre_storagebox')
add('bedroom', 'Bunker Bed', 'gr_prop_bunker_bed_01')
add('bedroom', 'Camp Bed', 'bkr_prop_biker_campbed_01')
-- ---------------- BATHROOM ----------------
add('bathroom', 'Toilet', 'prop_toilet_01')
add('bathroom', 'Toilet 2', 'prop_toilet_02')
add('bathroom', 'Old Toilet', 'prop_ld_toilet_01')
add('bathroom', 'Bathtub', 'apa_mp_h_bathtub_01')
add('bathroom', 'Towel Rail', 'prop_towel_rail_01')
add('bathroom', 'Towel Rail 2', 'prop_towel_rail_02')
add('bathroom', 'Hand Dryer', 'prop_handdry_01')
add('bathroom', 'Hand Dryer 2', 'prop_handdry_02')
add('bathroom', 'Shower Rack', 'prop_shower_rack_01')
add('bathroom', 'Soap Dispenser', 'prop_soap_disp_01')
add('bathroom', 'Toilet Roll', 'prop_toilet_roll_01')
add('bathroom', 'Toilet Brush', 'prop_toilet_brush_01')
add('bathroom', 'Washbasin', 'prop_sink_04')
add('bathroom', 'Washbasin 2', 'prop_sink_05')
add('bathroom', 'Washbasin 3', 'prop_sink_06')
add('bathroom', 'Washing Machine', 'prop_washer_01')
add('bathroom', 'Washing Machine 2','prop_washer_02')
add('bathroom', 'Washing Machine 3','prop_washer_03')
-- ---------------- KITCHEN ----------------
add('kitchen', 'Frying Pan', 'prop_kitch_pot_fry')
add('kitchen', 'Large Pot', 'prop_kitch_pot_lrg')
add('kitchen', 'Medium Pot', 'prop_kitch_pot_med')
add('kitchen', 'Small Pot', 'prop_kitch_pot_sm')
add('kitchen', 'Juicer', 'prop_kitch_juicer')
add('kitchen', 'Chopping Board', 'v_res_mchopboard')
add('kitchen', 'Knife Rack', 'v_res_mkniferack')
add('kitchen', 'Coffee Cup', 'v_res_mcofcup')
add('kitchen', 'Small Fridge', 'v_res_fridgemodsml')
add('kitchen', 'Modern Fridge', 'v_res_fridgemoda')
add('kitchen', 'Bar Fridge 2', 'prop_bar_fridge_02')
add('kitchen', 'Bar Fridge 3', 'prop_bar_fridge_03')
add('kitchen', 'Bar Fridge 4', 'prop_bar_fridge_04')
add('kitchen', 'Juice Dispenser', 'prop_juice_dispenser')
add('kitchen', 'Slush Machine', 'prop_slush_dispenser')
add('kitchen', 'Fruit Bowl', 'apa_mp_h_acc_fruitbowl_01')
add('kitchen', 'Fruit Bowl 2', 'apa_mp_h_acc_fruitbowl_02')
add('kitchen', 'Ceramic Bowl', 'apa_mp_h_acc_bowl_ceramic_01')
add('kitchen', 'Espresso Machine', 'apa_mp_h_acc_coffeemachine_01')
add('kitchen', 'Drinks Tray', 'apa_mp_h_acc_drink_tray_02')
add('kitchen', 'Soda Machine', 'prop_vend_soda_01')
add('kitchen', 'Soda Machine 2', 'prop_vend_soda_02')
add('kitchen', 'Coffee Vending', 'prop_vend_coffe_01')
add('kitchen', 'Snack Machine', 'prop_vend_snak_01')
-- ---------------- ELECTRONICS ----------------
family('tech', 'Old TV', 'prop_tv_%02d%s', 1, 7)
family('tech', 'Studio Light', 'prop_studio_light_%02d%s', 1, 3)
family('tech', 'DJ Deck', 'prop_dj_deck_%02d%s', 1, 2)
add('tech', 'Michael\'s TV', 'prop_tv_flat_michael')
add('tech', 'Big Flat TV', 'xm_prop_x17_tv_flat_01')
add('tech', 'Big Flat TV 2', 'xm_prop_x17_tv_flat_02')
add('tech', 'TV Unit', 'apa_mp_h_str_avunitl_01_b')
add('tech', 'TV Unit 2', 'apa_mp_h_str_avunitm_01')
add('tech', 'TV Unit Small', 'apa_mp_h_str_avunits_01')
add('tech', 'Hi-Fi', 'prop_hifi_01')
add('tech', 'Radio', 'prop_radio_01')
add('tech', 'Ghetto Blaster', 'prop_ghettoblast_02')
add('tech', 'MP3 Dock', 'prop_mp3_dock')
add('tech', 'Game Console', 'prop_console_01')
add('tech', 'Controller', 'prop_controller_01')
add('tech', 'Desk Phone', 'apa_mp_h_acc_phone_01')
add('tech', 'Office Phone', 'prop_office_phone_tnt')
add('tech', 'Fan', 'prop_fan_01')
add('tech', 'Jukebox', 'prop_jukebox_01')
add('tech', 'Jukebox 2', 'prop_jukebox_02')
add('tech', 'Lester\'s Laptop', 'prop_laptop_lester')
add('tech', 'Lester\'s Laptop 2', 'prop_laptop_lester2')
add('tech', 'Monitor Wall', 'prop_ld_monitor_01')
add('tech', 'Copier', 'prop_copier_01')
add('tech', 'ATM', 'prop_atm_01')
add('tech', 'Pinball', 'prop_pinballmachine')
-- ---------------- LIGHTING ----------------
add('light', 'Floor Lamp A', 'apa_mp_h_floorlamp_a')
add('light', 'Floor Lamp B', 'apa_mp_h_floorlamp_b')
add('light', 'Floor Lamp C', 'apa_mp_h_floorlamp_c')
for _, n in ipairs({ '01', '02', '03', '05', '06', '10', '13', '17' }) do
add('light', 'Designer Floor Lamp ' .. n, 'apa_mp_h_lit_floorlamp_' .. n)
end
for _, n in ipairs({ '005', '02', '04', '09', '14', '17', '21' }) do
add('light', 'Designer Table Lamp ' .. n, 'apa_mp_h_lit_lamptable_' .. n)
end
add('light', 'Night Lamp', 'apa_mp_h_lit_lamptablenight_16b')
add('light', 'Night Lamp 2', 'apa_mp_h_lit_lamptablenight_24')
add('light', 'Pendant Light', 'apa_mp_h_lit_lightpendant_01')
add('light', 'Pendant Light 2', 'apa_mp_h_lit_lightpendant_05')
add('light', 'Pendant Light 3', 'apa_mp_h_lit_lightpendant_05b')
add('light', 'Yacht Floor Lamp', 'apa_mp_h_yacht_floor_lamp_01')
add('light', 'Yacht Table Lamp', 'apa_mp_h_yacht_table_lamp_01')
add('light', 'Yacht Table Lamp 2', 'apa_mp_h_yacht_table_lamp_02')
add('light', 'Yacht Table Lamp 3', 'apa_mp_h_yacht_table_lamp_03')
add('light', 'Lamp (on)', 'v_res_fh_lampa_on')
add('light', 'Work Light', 'prop_worklight_03a')
add('light', 'Work Light 2', 'prop_worklight_04a')
add('light', 'Spotlight', 'prop_spot_01')
-- ---------------- DECOR ----------------
for _, n in ipairs({ '01', '02', '04', '05', '06' }) do
add('decor', 'Designer Vase ' .. n, 'apa_mp_h_acc_vase_' .. n)
end
family('decor', 'Flower Vase', 'apa_mp_h_acc_vase_flowers_%02d%s', 1, 4)
family('decor', 'Wool Rug', 'apa_mp_h_acc_rugwoolm_%02d%s', 1, 4)
family('decor', 'Small Wool Rug', 'apa_mp_h_acc_rugwools_%02d%s', 1, 3)
family('decor', 'Large Wool Rug', 'apa_mp_h_acc_rugwooll_%02d%s', 3, 4)
family('decor', 'Large Artwork', 'apa_mp_h_acc_artwalll_%02d%s', 1, 3)
family('decor', 'Artwork', 'apa_mp_h_acc_artwallm_%02d%s', 2, 4)
family('decor', 'Sculpture', 'apa_mp_h_acc_dec_sculpt_%02d%s', 1, 3)
family('decor', 'Decor Plate', 'apa_mp_h_acc_dec_plate_%02d%s', 1, 2)
family('decor', 'Candles', 'apa_mp_h_acc_candles_%02d%s', 1, 6)
family('decor', 'Decor Jar', 'apa_mp_h_acc_jar_%02d%s', 2, 4)
family('decor', 'Trinket Box', 'apa_mp_h_acc_box_trinket_%02d%s', 1, 2)
family('decor', 'Decor Bottle', 'apa_mp_h_acc_bottle_%02d%s', 1, 2)
add('decor', 'Palm (indoor)', 'apa_mp_h_acc_plant_palm_01')
add('decor', 'Tall Plant', 'apa_mp_h_acc_plant_tall_01')
add('decor', 'Head Bust', 'apa_mp_h_acc_dec_head_01')
add('decor', 'Pot Pourri', 'apa_mp_h_acc_pot_pouri_01')
add('decor', 'Scent Sticks', 'apa_mp_h_acc_scent_sticks_01')
add('decor', 'Decor Tray', 'apa_mp_h_acc_tray_01')
add('decor', 'Fresh Flowers', 'v_res_m_vasefresh')
add('decor', 'Chinese Vase', 'v_res_mvasechinese')
add('decor', 'House Plant', 'v_res_tre_plant')
add('decor', 'Basket', 'v_res_tre_flatbasket')
add('decor', 'Pouf', 'v_res_fh_pouf')
add('decor', 'Skateboard', 'v_res_skateboard')
add('decor', 'Golf Bag', 'prop_golf_bag_01')
add('decor', 'Acoustic Guitar', 'prop_acc_guitar_01')
family('decor', 'Electric Guitar', 'prop_el_guitar_%02d%s', 1, 3)
add('decor', 'Palm A', 'prop_plant_palm_01a')
add('decor', 'Palm B', 'prop_plant_palm_01b')
add('decor', 'Palm C', 'prop_plant_palm_01c')
add('decor', 'Paradise Plant', 'prop_plant_paradise')
add('decor', 'Paradise Plant 2', 'prop_plant_paradise_b')
family('decor', 'Flowers', 'prop_plant_flower_%02d%s', 1, 4)
add('decor', 'Cane Plant', 'prop_plant_cane_01a')
add('decor', 'Cane Plant 2', 'prop_plant_cane_01b')
add('decor', 'Office Award', 'ex_prop_exec_award_gold')
add('decor', 'Office Award 2', 'ex_prop_exec_award_silver')
add('decor', 'City Model', 'ex_office_citymodel_01')
add('decor', 'Floor Safe', 'prop_ld_int_safe_01')
-- ---------------- OFFICE ----------------
family('office', 'Filing Cabinet', 'prop_cabinet_%02d%s', 1, 2, AB)
family('office', 'Executive Chair','ex_prop_offchair_exec_%02d%s', 1, 4)
add('office', 'Office Chair Black','prop_cs_office_chair')
add('office', 'Heist Office Chair','hei_prop_heist_off_chair')
add('office', 'Whiteboard', 'prop_whiteboard')
add('office', 'Water Dispenser', 'prop_watercooler_dark')
add('office', 'Shredder', 'prop_shredder')
add('office', 'Till', 'prop_till_01')
add('office', 'Till 2', 'prop_till_02')
add('office', 'Cash Counter', 'bkr_prop_money_counter')
add('office', 'Bunker Desk', 'gr_prop_gr_bench_04a')
add('office', 'Mechanic Workbench','prop_tool_bench02')
add('office', 'Tool Chest', 'prop_toolchest_01')
add('office', 'Tool Chest 2', 'prop_toolchest_02')
add('office', 'Tool Chest 3', 'prop_toolchest_03')
-- ---------------- FITNESS ----------------
family('fitness', 'Muscle Bench', 'prop_muscle_bench_%02d%s', 1, 6)
family('fitness', 'Yoga Mat', 'prop_yoga_mat_%02d%s', 1, 3)
family('fitness', 'Weight Rack', 'prop_weight_rack_%02d%s', 1, 2)
add('fitness', 'Exercise Bike', 'prop_exer_bike_01')
add('fitness', 'Barbell', 'prop_barbell_01')
add('fitness', 'Barbell 2', 'prop_barbell_02')
add('fitness', 'Barbell 60 kg', 'prop_barbell_60kg')
add('fitness', 'Barbell 80 kg', 'prop_barbell_80kg')
add('fitness', 'Barbell 100 kg', 'prop_barbell_100kg')
add('fitness', 'Dumbbell 2', 'prop_dumbbell_02')
-- ---------------- GARDEN ----------------
family('garden', 'BBQ', 'prop_bbq_%d%s', 1, 5)
family('garden', 'Parasol', 'prop_parasol_%02d%s', 1, 5, AB)
family('garden', 'Beach Parasol', 'prop_beach_parasol_%02d%s', 1, 10)
family('garden', 'Sun Lounger', 'prop_patio_lounger%d%s', 1, 3)
family('garden', 'Planter', 'prop_planter_%02d%s', 1, 2, { 'a', 'b', 'c' })
family('garden', 'Garden Gnome', 'prop_gnome%d%s', 1, 3)
family('garden', 'Fountain', 'prop_fountain%d%s', 1, 2)
family('garden', 'Beach Towel', 'prop_beach_towel_%02d%s', 1, 4)
add('garden', 'Patio Heater', 'prop_patio_heater_01')
add('garden', 'Hot Tub', 'prop_hottub2')
add('garden', 'Dog House', 'prop_doghouse_01')
add('garden', 'Dream Catcher', 'prop_garden_dreamcatch_01')
add('garden', 'Garden Light', 'prop_gardnght_01')
add('garden', 'Wind Chimes', 'prop_garden_chimes_01')
add('garden', 'Beach Lilo', 'prop_beach_lilo_01')
add('garden', 'Yacht Lounger', 'prop_yacht_lounger')
add('garden', 'Skid Chair', 'prop_skid_chair_01')
add('garden', 'Trampoline', 'prop_trampoline_01')
add('garden', 'Swing Set', 'prop_swingset_01')
add('garden', 'Basketball Hoop', 'prop_bskball_01')
add('garden', 'Garden Hose', 'prop_hose_1')
add('garden', 'Lawn Mower', 'prop_lawnmower_01')
add('garden', 'Wheelbarrow', 'prop_wheelbarrow01a')
-- ---------------- ORDER ----------------
-- Within each category, things of one kind stand together -- every sofa,
-- then every armchair, then the chairs -- instead of in the order they were
-- added above. The first word of each list that a label contains decides its
-- group; numbers sort naturally (Chair 2 before Chair 10).
local KINDS = {
seating = { 'sofa', 'couch', 'daybed', 'armchair', 'easy chair', 'club chair', 'office chair',
'executive', 'chair', 'stool', 'bench', 'lounger', 'beanbag' },
tables = { 'dining', 'dinner', 'eames', 'coffee', 'side table', 'desk', 'dressing', 'worktop',
'folding', 'picnic', 'patio', 'market', 'pool', 'poker', 'tennis', 'table' },
bedroom = { 'bed', 'mattress', 'nightstand', 'bedside', 'wardrobe', 'closet', 'dresser', 'drawers',
'sideboard', 'shelf', 'cabinet', 'storage', 'mirror' },
bathroom = { 'toilet', 'bathtub', 'washbasin', 'towel', 'shower', 'soap', 'hand dryer', 'washing' },
kitchen = { 'fridge', 'cooker', 'microwave', 'sink', 'counter', 'coffee', 'espresso', 'kettle',
'toaster', 'juic', 'pan', 'pot', 'bowl', 'knife', 'chopping', 'tray', 'cup', 'bin',
'vending', 'soda', 'snack', 'slush', 'wine', 'bar', 'water' },
tech = { 'tv', 'monitor', 'pc', 'laptop', 'keyboard', 'printer', 'copier', 'speaker', 'hi-fi',
'radio', 'stereo', 'blaster', 'dock', 'console', 'controller', 'phone', 'dj', 'studio',
'arcade', 'pinball', 'jukebox', 'fan', 'atm', 'dartboard' },
light = { 'floor lamp', 'table lamp', 'night lamp', 'desk lamp', 'lamp', 'pendant', 'ceiling',
'wall light', 'candle', 'spot', 'work light', 'neon' },
decor = { 'rug', 'painting', 'artwork', 'sculpture', 'bust', 'statue', 'vase', 'flower', 'plant',
'palm', 'fern', 'cactus', 'paradise', 'cane', 'book', 'shelf', 'clock', 'candle', 'jar',
'bottle', 'plate', 'trinket', 'tray', 'scent', 'pot pourri', 'box', 'crate', 'safe',
'globe', 'aquarium', 'fireplace', 'guitar', 'golf', 'skateboard', 'award', 'model',
'curtain', 'pouf', 'basket', 'gun', 'dumbbell', 'punching', 'weight' },
office = { 'desk', 'executive', 'office chair', 'chair', 'filing', 'cabinet', 'whiteboard', 'water',
'shredder', 'till', 'cash', 'tool', 'workbench' },
fitness = { 'bench', 'barbell', 'dumbbell', 'weight', 'bike', 'yoga' },
garden = { 'bbq', 'parasol', 'lounger', 'chair', 'table', 'planter', 'fountain', 'gnome', 'light',
'chimes', 'dream', 'hot tub', 'trampoline', 'swing', 'basketball', 'dog', 'hose', 'mower',
'wheelbarrow', 'towel', 'lilo' },
}
local function kindOf(entry)
local label = entry.label:lower()
for i, word in ipairs(KINDS[entry.cat] or {}) do
if label:find(word, 1, true) then return i end
end
return 999
end
local function naturalKey(label)
return label:lower():gsub('%d+', function(n) return ('%06d'):format(tonumber(n)) end)
end
do
local catIndex = {}
for i, c in ipairs({ 'seating', 'tables', 'bedroom', 'bathroom', 'kitchen', 'tech', 'light',
'decor', 'office', 'fitness', 'garden' }) do catIndex[c] = i end
for i, e in ipairs(Config.Furniture) do
e._c, e._k, e._n, e._i = catIndex[e.cat] or 99, kindOf(e), naturalKey(e.label), i
end
table.sort(Config.Furniture, function(a, b)
if a._c ~= b._c then return a._c < b._c end
if a._k ~= b._k then return a._k < b._k end
if a._n ~= b._n then return a._n < b._n end
return a._i < b._i
end)
for _, e in ipairs(Config.Furniture) do e._c, e._k, e._n, e._i = nil, nil, nil, nil end
end
Config.FurnitureCategories = { 'seating', 'tables', 'bedroom', 'bathroom', 'kitchen', 'tech', 'light', 'decor', 'office', 'fitness', 'garden' }
--==============================================================================
-- 8 · UPGRADES & CAMERAS
--==============================================================================
-- ── Camera module ────────────────────────────────────────────────────────────
-- Bought once per house, paid from Config.Account. The camera module comes
-- with a front door camera; further angles are placed by the owner.
Config.Upgrades = {
cameras = {
label = 'Security Cameras',
price = 35000,
note = 'Front door camera included. Up to %s more at $%s each.',
},
}
Config.MaxCameras = 4 -- custom cameras on top of the door camera
Config.CameraPrice = 5000 -- charged for every camera beyond the included one
Config.CameraPanSpeed = 0.45 -- degrees per frame when panning a live feed with A/D
Config.CameraReach = 5.0 -- how far from the cursor a camera hangs in open air
-- ── Live feed, door camera & camera prop ─────────────────────────────────────
-- How a camera feed looks: one of GTA's timecycle modifiers, e.g.
-- 'scanline_cam_cheap' (security camera), 'CAMERA_secuirity' (green CCTV),
-- or false for the plain picture. Switching feeds always shows GTA's static.
Config.CameraFeed = {
filter = 'scanline_cam_cheap',
strength = 1.0,
-- GTA's security camera switch sound, played with the static for `ms`
-- milliseconds; false = none
sound = { name = 'Change_Cam', set = 'MP_CCTV_SOUNDSET', ms = 450 },
-- GTA's camera motor while panning with A/D; false = none
panSound = { name = 'Pan', set = 'MP_CCTV_SOUNDSET' },
}
-- Where the included door camera sits, relative to the entrance.
--
-- Straight above the door puts it inside the porch roof on a lot of houses, so
-- it is pulled back away from the building as well. The client additionally
-- checks the spot for geometry and pulls it back further if it is buried.
Config.DoorCamera = {
height = 2.05, -- metres above the entrance
back = 1.45, -- metres away from the door, towards the street
pitch = -26.0, -- tilted down onto the doorstep
label = 'Front Door',
}
-- A visible camera on the wall. Set enabled = false for invisible cameras.
Config.CameraProp = {
enabled = true,
model = 'prop_cctv_cam_06a',
headingOffset = 180.0,
drop = 0.0,
}
--==============================================================================
-- 9 · STORAGE & WARDROBE
--==============================================================================
-- Both are optional points inside the house, placed by the owner from their own
-- panel (F3) rather than by an admin in the creator: they are fittings, not
-- architecture, and the person living there knows where the box belongs. Only
-- the owner may move them; owner and key holders may use them, the same rule
-- the garage follows.
--
-- Setting `enabled = false` here removes the row from the owner panel as well,
-- so a server without stashes never offers one.
--
-- Found by themselves, checked each time a point is used:
-- Storage ox_inventory, qb-inventory (1.x and 2.x), qs-inventory,
-- ps-inventory, lj-inventory, jpr-inventory. With none of them
-- running, storage is not offered at all: without an inventory
-- there are no items to put in it (always the case standalone).
-- Wardrobe illenium-appearance, then qb-clothing, then the built-in
-- wardrobe, which needs no clothing script and works standalone.
--
-- `open` is an override. Leave it nil to use the detection; fill it in for a
-- resource that is not on the list, and nothing else in the script cares how
-- the menu is opened.
Config.Stash = {
enabled = true,
label = 'Storage',
slots = 50,
maxWeight = 400000, -- grams, matching QB weight units
--- Stash id. Per house, so a key holder reaches the same box as the owner.
id = function(house) return ('agency_house_%s'):format(house.id) end,
--- Empty the box when the house is sold. The stash id is per house, so
--- without this the next buyer inherits whatever the last owner left in it.
---
--- nil = ox_inventory is emptied automatically (it has exactly one way to
--- do it); every other inventory is left alone, because each one
--- clears a stash differently and the wrong call silently does
--- nothing.
--- false = never empty the box.
--- A function = your own way. Fill in the one line your inventory needs:
---
--- qs-inventory : exports['qs-inventory']:ClearStash(stashId)
--- qb-inventory : MySQL.query('DELETE FROM stashitems WHERE stash = ?', { stashId })
---
--- clear = function(house, stashId) ... end,
---
--- The house table is passed too, for inventories that key off the owner.
clear = nil,
--- Open the box. nil = found by itself (see the list above). Client side.
--- For an inventory that is not on the list, e.g.:
---
--- open = function(house, stashId)
--- exports['my-inventory']:OpenStash(stashId, Config.Stash.slots, Config.Stash.maxWeight)
--- end,
open = nil,
}
Config.Wardrobe = {
enabled = true,
label = 'Wardrobe',
--- The wardrobe this script brings along. Saves what the character wears
--- under a name and puts it back on, with no clothing script at all. Used
--- only when neither illenium-appearance nor qb-clothing runs. It restores
--- how the character looks; it does not change a clothing script's saved
--- skin. false = no wardrobe on servers without a clothing script.
--- To use it even though a clothing script runs, put this in server.cfg
--- (or type it into the live console, it then lasts until the restart):
--- setr agency_housing_wardrobe builtin
builtin = true,
maxOutfits = 20, -- per player, across all their houses
--- Command that opens the built-in wardrobe right where you stand, no
--- wardrobe point needed. Admins only unless commandForEveryone = true:
--- a wardrobe that opens anywhere lets players change clothes anywhere.
--- '' = no command.
command = 'ahwardrobe',
commandForEveryone = false,
--- Open the outfit menu. nil = found by itself (see the list above).
--- Client side. For a clothing script that is not on the list, e.g.:
---
--- open = function(house)
--- TriggerEvent('my-clothing:openOutfits')
--- end,
open = nil,
}
--==============================================================================
-- 10 · GARAGE
--==============================================================================
-- A house can have a garage point recorded in the creator. Standing on it, the
-- owner and their keyholders get a prompt that hands over to whatever garage
-- script the server runs — this resource does not manage vehicles itself.
Config.Garage = {
enabled = true,
label = 'Garage',
-- Which garage script keeps the cars of a house garage.
-- 'auto' the first one that is started, in this order:
-- Agency-Garage, jg-advancedgarages, qb-garages
-- 'agency' Agency-Garage
-- 'jg' jg-advancedgarages (v3 garage API; older versions through
-- their open-garage / store-vehicle events)
-- 'qb' qb-garages 1.x or 2.x. qb-houses is not needed: this resource
-- answers the key check qb-garages asks qb-houses for.
-- 'custom' the functions in `custom` below
-- 'event' only fires agency-housing:garage:open (house id, house name)
-- for a script of your own
-- With 'auto' and none of them running, the garage point is not shown.
-- The console says on start which one was picked.
script = 'auto',
-- Resource names, for a server that renamed a folder.
resources = {
agency = 'Agency-Garage',
jg = 'jg-advancedgarages',
qb = 'qb-garages',
},
-- What Agency-Garage should know about a house garage.
-- `access` is deliberately absent: this resource already draws the marker
-- and checks who owns the house, so the garage script must not add a second
-- prompt on the same spot.
--
-- The car appears on the garage point itself, deliberately without any
-- offset. An offset keeps the point's height while moving sideways, so on
-- a garage under a building it puts the car through the ceiling and onto
-- the roof. The garage script only treats a point as blocked when another
-- vehicle stands within 2.6 m -- a player standing there is fine -- so the
-- point is the one spot guaranteed to be on the right floor.
template = {
type = 'public',
category = 'car',
},
-- script = 'custom' only. Every function gets the house and its garage id
-- (house_<id>); the garage point is house.garage = { x, y, z, h }.
custom = {
register = nil, -- client, on every house sync
registerServer = nil, -- server, on load and when the point changes
open = nil, -- client, [E] on foot at the point
park = nil, -- client, [E] in a vehicle; nil = no park prompt
},
-- The short camera move played while a car is handed out. The player is
-- hidden and stepped aside for it, because the car appears exactly where
-- they are standing and would otherwise trap them inside it.
handover = {
enabled = true,
distance = 6.0, -- how far the camera sits from the car
height = 1.9, -- how far above it
swing = 38.0, -- degrees the camera travels around the car
entry = 320, -- glide from the player's view into the shot
time = 1400, -- milliseconds the swing may take AT MOST
wait = 5000, -- ceiling when the shot starts before the car is
-- ordered, so it covers the trip to the server
linger = 140, -- how long the camera holds once the car is there
fade = 220, -- blend back to the player view, in milliseconds
standoff = 3.0, -- where the player is put down next to the car
},
}
--==============================================================================
-- 11 · APPS
--==============================================================================
-- ── Phone & Pad app ──────────────────────────────────────────────────────────
-- While this resource runs, a "Housing" app is offered in the App Store of
-- Agency Phone and Agency Pad: every home the player owns, rents or holds a
-- key to, lock and unlock from anywhere, a waypoint home and the security
-- cameras live. Players download it themselves; it stays until they delete
-- it. Needs Agency Phone / Agency Pad with app SDK 2 (27.0.2 or newer).
Config.PhoneApp = {
enabled = true,
label = 'Housing',
icon = 'fa-solid fa-house-chimney', -- FontAwesome, on the colour below
color = '#e2b649',
colorDark = '#a8801f',
category = 'lifestyle',
-- Space above the app for the status bar, in pixels (phone / pad).
topInset = 50,
padTopInset = 28,
-- After watching cameras from the app the phone stays put away (false).
-- true tries to bring the app back; that can leave some phones half open.
reopenAfterCameras = false,
-- Other phone scripts the app is offered in, found automatically. Set one
-- to false to leave it out, or to a string if its resource is renamed.
-- lb = lb-phone (custom apps), qs = qs-smartphone-pro (custom apps)
phones = {
lb = 'lb-phone',
qs = 'qs-smartphone-pro',
},
qsCategory = 'social', -- app store category in qs-smartphone-pro
iconUrl = nil, -- own icon (https:// or nui://); nil = nui/app/icon.png
-- Any other phone (README: "The Housing app in other phones"). For the
-- cameras the phone has to be put away and brought back; fill these in
-- with your phone's exports, e.g.
-- closePhone = function() exports['my-phone']:Close() end,
-- reopenPhone = function() exports['my-phone']:OpenApp('agency_housing') end,
-- isPhoneOpen = function() return exports['my-phone']:IsOpen() end,
closePhone = nil,
reopenPhone = nil,
isPhoneOpen = nil,
}
-- ── Agency Companion app ─────────────────────────────────────────────────────
-- The Agency Companion app (iOS / Android, companion.agencyg.de) links to
-- this housing system with its OWN QR code, independent of Agency Phone.
-- Players lock and unlock their houses, hand out and take back keys,
-- answer the doorbell, buy and sell, and set a route to any property from
-- the phone; admins teleport to a house, list it for sale or delete it.
-- Every action runs the same server-side checks as the in-game panel: the
-- pairing can never grant anything the character does not have.
--
-- Players open the pairing card from the hub (F7 > Companion App) or with
-- /ahconnect. The server dials OUT to the relay over HTTPS, nothing has to
-- be forwarded. Set Enable = false to opt out entirely.
Config.Companion = {
Enable = true, -- master switch for the companion integration
EnableRemote = true, -- dial out to the relay (false = QR shows but no remote control)
RelayUrl = 'https://api.agencyg.de', -- Agency relay endpoint (leave as-is unless self-hosting)
TokenTTL = 300, -- seconds a pairing QR / 6-digit code stays valid
-- The collapsible pairing card inside the owner panel (F3) and the
-- property list, so nobody has to know the hub tile or /ahconnect.
-- false hides it there; the hub tile and /ahconnect stay available.
EmbedCard = true,
-- How close (metres) another player has to stand to receive a key from
-- the app. The same distance the in-game panel uses.
KeyDistance = 5.0,
}
02SQL File
Optional: the resource creates these tables on its own when it starts. Import this file only if you set up the database by hand:
-- =====================================================
-- Agency Housing: database tables
-- =====================================================
--
-- Optional. Agency-Housing creates all of these on start and adds any column an
-- older database is missing, so a fresh server needs no import. Run this by
-- hand only if your database user is not allowed to create tables.
--
-- Safe to run more than once: CREATE TABLE IF NOT EXISTS leaves an existing
-- table and its data alone. Works on MariaDB and MySQL 8.
--
-- If you changed Config.TableName, change `agency_houses` below to match.
-- Houses, their owners, keys and everything placed in them.
CREATE TABLE IF NOT EXISTS `agency_houses` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`shell` VARCHAR(64) NOT NULL,
`price` INT NOT NULL DEFAULT 0,
`entrance` LONGTEXT NOT NULL,
`interior` LONGTEXT DEFAULT NULL,
`owner` VARCHAR(80) DEFAULT NULL,
`owner_name` VARCHAR(100) DEFAULT NULL,
`locked` TINYINT(1) NOT NULL DEFAULT 1,
`for_sale` TINYINT(1) NOT NULL DEFAULT 1,
`show_blip` TINYINT(1) NOT NULL DEFAULT 1,
`keys` LONGTEXT DEFAULT NULL,
`created_by` VARCHAR(100) DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`garage` LONGTEXT DEFAULT NULL,
`exit_point` LONGTEXT DEFAULT NULL,
`stash_point` LONGTEXT DEFAULT NULL,
`wardrobe_point` LONGTEXT DEFAULT NULL,
`zone` LONGTEXT DEFAULT NULL,
`kind` VARCHAR(16) NOT NULL DEFAULT 'interior',
`furniture` LONGTEXT DEFAULT NULL,
`upgrades` LONGTEXT DEFAULT NULL,
`cameras` LONGTEXT DEFAULT NULL,
`doors` LONGTEXT DEFAULT NULL,
`blip` LONGTEXT DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `owner` (`owner`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Who disconnected inside which house, so they are put back on rejoin.
CREATE TABLE IF NOT EXISTS `agency_house_inside` (
`identifier` VARCHAR(80) NOT NULL,
`house_id` INT NOT NULL,
`since` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`identifier`),
KEY `house_id` (`house_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Outfits of the built-in wardrobe.
CREATE TABLE IF NOT EXISTS `agency_house_outfits` (
`id` INT NOT NULL AUTO_INCREMENT,
`identifier` VARCHAR(80) NOT NULL,
`name` VARCHAR(40) NOT NULL,
`model` INT NOT NULL,
`data` LONGTEXT NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Agency Companion app: this server's id and the linked phones.
CREATE TABLE IF NOT EXISTS `agency_housing_cmp_meta` (
`k` VARCHAR(64) NOT NULL PRIMARY KEY,
`v` TEXT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `agency_housing_cmp_devices` (
`id` INT NOT NULL AUTO_INCREMENT,
`identifier` VARCHAR(60) NOT NULL,
`player_name` VARCHAR(80) DEFAULT NULL,
`token_hash` CHAR(64) NOT NULL,
`device_name` VARCHAR(64) DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_seen` TIMESTAMP NULL DEFAULT NULL,
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `token_hash` (`token_hash`),
KEY `identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;