Garage V2
aty_garage_v2
Full-featured multi-framework garage system: public/private/manageable/job/impound/lost-vehicle garages, pre-built IPL interior garages, park-in-place persistence, vehicle insurance, key sharing, shared-garage members, ps-housing house garages, and a police fine/impound workflow — all driven by a Vue NUI and the aty_lib framework bridge.
Info
|
|
| Resource name |
aty_garage_v2 |
| Frameworks |
ESX / QBCore / Qbox (via qbx_core qb-core compat bridge) — detected through aty_lib |
| Database |
MySQL (oxmysql/mysql-async, via aty_lib → modules.ExecuteSql) |
Dependencies
| Dependency |
Required |
Notes |
aty_lib |
Yes (declared in fxmanifest.lua) |
Framework bridge: getModules(), getUtils(), getShared() — SQL helper, callbacks (modules.RegisterCallback/TriggerCallback), notifications, identifiers, player job/gang lookup |
qb-core / es_extended / qbx_core |
One of these |
Framework detected via Utils.Framework ('qb-core' or 'es_extended'); Qbox works through its qb-core compat layer |
ox_target or qb-target |
Optional |
Only needed if Config.Interaction = 'target'. Auto-detected; falls back to marker mode if neither is running |
ps-housing |
Optional |
Only needed if Config.Housing.enabled = true — reads its properties table for house garages |
okokBilling / esx_billing / qb phone_invoices |
Optional |
Used by the police billing bridge (server/billing.lua) when Config.Police.billing = 'auto' (or a specific mode) |
Installation
Copy the aty_garage_v2 folder into your resources (e.g. [aty]/aty_garage_v2).
No manual SQL import is required — on first start the resource runs CREATE TABLE IF NOT EXISTS statements itself (in server/main.lua) for all of its tables: aty_garages, aty_garage_ipls, aty_garage_vehmeta, aty_garage_members, aty_garage_keys, aty_garage_parked, aty_garage_fines, aty_garage_impounds. It also auto-migrates a few legacy columns (e.g. adds gang/gang_grade to aty_garages, stored_at/insurance_until/destroyed to aty_garage_vehmeta if missing). It reads/writes vehicle ownership from your framework's existing table: player_vehicles (QBCore) or owned_vehicles (ESX).
Add to server.cfg, making sure aty_lib starts first:
ensure aty_lib
ensure aty_garage_v2
Configure shared/config.lua (client + server visible) and server/config.lua (server-only — Discord webhook settings; never sent to clients).
If using the pre-built IPL garages, verify/adjust the interior coordinates for your server with the /iploc and /iplhere diagnostic commands (see Commands), then place the actual menu/preview/spawn/zone points per IPL with /garageipl.
Create garages in-game with /garageadmin (or the command configured in Config.AdminCommand).
Configuration
shared/config.lua — General
| Option |
Type |
Default |
Description |
Debug |
boolean |
false |
Debug flag (used sparingly in code) |
Language |
string |
'en' |
Active locale ('en' | 'tr'), loaded from locales/<lang>.lua; missing keys fall back to EN via locales/loader.lua |
Interaction
| Option |
Type |
Default |
Description |
Interaction |
string |
'target' |
'marker' (3D marker + key prompt) or 'target' (ox_target/qb-target zones, auto-detected; falls back to marker if no target resource is running) |
TargetIcons |
table |
{ open, enter, store, buy, impound } |
FontAwesome icon per target action (keys: open, enter, store, buy, impound) |
InteractionKey |
number |
38 (E) |
Key to open/interact in marker mode |
InteractionDistance |
number |
3.0 |
Distance to open a garage menu / enter an IPL |
StoreDistance |
number |
8.0 |
Distance (in a vehicle) to store/park |
MarkerDrawDistance |
number |
15.0 |
Distance at which world markers are drawn |
Admin
| Option |
Type |
Default |
Description |
AdminCommand |
string |
'garageadmin' |
Command that opens the garage creation/admin menu |
IplCommand |
string |
'garageipl' |
Command that opens the IPL interior editor directly |
QBPermission |
string/table |
{ 'admin', 'god' } |
QBCore permission level(s) for AdminCommand/IplCommand (registered via QBCore.Commands.Add) |
ESXGroup |
string/table |
{ 'admin', 'superadmin' } |
ESX group(s) for the same commands (registered via ESX.RegisterCommand) |
AdminAce |
string |
'aty_garage.admin' |
Extra ace permission checked as a defense-in-depth layer on admin callbacks |
AdminGroups |
table |
{ admin = true, god = true, superadmin = true } |
Framework group names accepted by the internal isAdmin() check |
PlaceholderVehicle |
string |
'adder' |
Model shown while placing IPL "Vehicle Spawn" points in the editor |
Vehicles & economy
| Option |
Type |
Default |
Description |
SharedGarages |
boolean |
false |
If true, a vehicle can be taken out directly from any garage regardless of where it was stored |
Warp |
boolean |
true |
Auto-warp the player into the vehicle after spawn |
VisuallyDamageCars |
boolean |
true |
Re-apply visual damage on spawn |
DefaultCapacity |
number |
25 |
Default garage vehicle capacity |
Bring.enabled |
boolean |
true |
Enable "bring vehicle from another garage" (used when SharedGarages = false) |
Bring.price |
number |
1500 |
Fee charged to bring a vehicle |
Bring.account |
string |
'bank' |
Account charged for Bring |
Owned.account |
string |
'bank' |
Account used for private garage purchase/sale |
Owned.maxHourlyFee |
number |
100 |
Cap on the hourly fee an owner can set on their private garage |
Owned.sellRefundPercent |
number |
50 |
Refund percentage when a private garage is sold |
JobPlatePrefix |
string |
'JOB' |
Plate prefix used for job fleet vehicles |
Lost.retrieveFee |
number |
2500 |
Fee to retrieve a "lost" vehicle (not impounded, not destroyed) into a normal garage |
Lost.account |
string |
'bank' |
Account charged for Lost retrieval |
Integration hooks
| Option |
Type |
Default |
Description |
Integrations.getFuel |
function | nil |
nil |
Override fuel read (falls back to aty_lib bridge default); errors are caught with pcall |
Integrations.setFuel |
function | nil |
nil |
Override fuel write |
Integrations.onVehicleSpawned |
function | nil |
nil |
Called (veh, plate, props) after a vehicle is spawned from a garage |
Integrations.onVehicleStored |
function | nil |
nil |
Called (veh, plate, props) right before a stored vehicle's entity is deleted |
Event hooks (Config.Hooks, client-side)
Every hook is optional (nil = disabled) and pcall-protected. Each one also fires a Lua event aty_garage_v2:hook:<name> with the same arguments, so other resources can listen with AddEventHandler without editing the config.
| Hook |
Arguments |
Fired when |
onMenuOpen |
(garage) |
The garage menu opens |
onMenuClose |
(garage) |
The garage menu closes |
onIplEnter |
(garage) |
The player enters an IPL interior |
onIplExit |
(garage) |
The player exits an IPL interior |
onLockpickStart |
(veh, plate) |
A lockpick attempt starts (after server validation) |
onLockpickSuccess |
(veh, plate) |
The lock is picked successfully |
onLockpickFail |
(veh, plate, broke) |
The attempt fails (broke = lockpick item broke) |
onLockpickAlert |
(coords, plate, veh) |
Police alert roll passes (Lockpick.alertChance) — wire to your dispatch script, e.g. exports['ps-dispatch']:CarJacking(veh) |
onVehicleTaken |
(veh, plate, props) |
A vehicle is taken out and spawned |
onVehicleStored |
(veh, plate, props) |
A vehicle is stored (entity still exists) |
Example — dispatch alert without touching the config, from any resource:
AddEventHandler('aty_garage_v2:hook:onLockpickAlert', function(coords, plate, veh)
exports['ps-dispatch']:CarJacking(veh)
end)
Housing (ps-housing)
| Option |
Type |
Default |
Description |
Housing.enabled |
boolean |
true |
Enable dynamic house garages generated from ps-housing's properties.garage_data |
Housing.script |
string |
'ps-housing' |
Only 'ps-housing' is currently supported |
Housing.capacity |
number |
10 |
Capacity of each house garage |
Housing.rescanInterval |
number (ms) |
300000 |
Periodic re-scan interval of the properties table |
Housing.blip |
boolean |
false |
Whether to show a map blip for house garages |
Insurance & vehicle recovery
| Option |
Type |
Default |
Description |
Insurance.enabled |
boolean |
true |
Enable per-vehicle insurance purchase |
Insurance.account |
string |
'bank' |
Account charged for insurance |
Insurance.price |
number |
5000 |
Policy price |
Insurance.durationDays |
number |
7 |
Policy duration in days (re-purchase extends it) |
Insurance.claimFee |
number |
1000 |
Fee to recover an insured, destroyed vehicle |
Insurance.allowUninsured |
boolean |
true |
Whether an uninsured destroyed vehicle can still be recovered (for uninsuredFee) |
Insurance.uninsuredFee |
number |
15000 |
Fee to recover an uninsured destroyed vehicle |
Insurance.cancelRefundPercent |
number |
50 |
Refund percentage (of remaining policy value) on cancellation |
Preview camera
| Option |
Type |
Default |
Description |
Preview.enabled |
boolean |
true |
Enable the vehicle preview camera in garage menus |
Preview.dof |
boolean |
false |
Depth-of-field (blur background, keep vehicle sharp) |
Preview.camOffset |
vector3 |
vec3(4.2, 3.6, 1.5) |
Camera offset from the previewed vehicle |
Preview.fov |
number |
45.0 |
Camera field of view |
Blips
| Option |
Type |
Default |
Description |
Blip.show |
boolean |
true |
Show garage blips on the map |
Blip.sprite |
number |
357 |
Blip sprite ID |
Blip.color |
number |
3 |
Blip color ID |
Blip.size |
number |
0.8 |
Blip scale |
Blip.toggleCommand |
string |
'garageblips' |
Client command to toggle blip visibility per player (preference persisted in a resource KVP) |
Blur (menu background)
| Option |
Type |
Default |
Description |
Blur.enabled |
boolean |
true |
Enable frosted-glass background blur in the NUI (captured via screenshot-basic + GLSL, not the native blur) |
Blur.interval |
number (ms) |
350 |
Recapture interval — lower = livelier but costs more performance |
Blur.settleDelay |
number (ms) |
250 |
Delay before the first capture after the menu opens (lets the camera settle) |
Mileage
| Option |
Type |
Default |
Description |
Mileage.enabled |
boolean |
true |
Enable client→server mileage tracking/sync |
Mileage.syncInterval |
number (ms) |
60000 |
Client sync interval |
Park-in-place
| Option |
Type |
Default |
Description |
ParkInPlace.enabled |
boolean |
true |
Store vehicles in the world at the spot they were parked instead of despawning them |
ParkInPlace.parkRadius |
number |
30.0 |
Max distance from the garage center a vehicle may be parked |
ParkInPlace.spawnDistance |
number |
60.0 |
Distance at which a nearby player triggers the entity to (re)spawn |
ParkInPlace.deleteDistance |
number |
85.0 |
Distance beyond which the entity is deleted (all players out of range) |
ParkInPlace.checkInterval |
number (ms) |
4000 |
Server-side distance check interval |
Police / fines / impound
| Option |
Type |
Default |
Description |
Police.jobs |
table |
{ police = true, sheriff = true } |
Jobs allowed to use fine/impound features |
Police.fineKey |
number |
47 (G) |
Key to open the fine/impound prompt near a parked vehicle |
Police.interactDistance |
number |
4.0 |
Distance for the fine/impound prompt |
Police.minFine / Police.maxFine |
number |
50 / 10000 |
Allowed fine amount range |
Police.payAccount |
string |
'bank' |
Internal collection account (used when no external billing succeeds) |
Police.reasons |
table |
4 presets (Illegal Parking 250, Blocking Traffic 400, Abandoned Vehicle 600, Parking on Sidewalk 300) |
Preset reasons shown in the fine/impound menu (amount editable) |
Police.impound.searchRadius |
number |
18.0 |
Radius to find a target vehicle for impound |
Police.impound.defaultEarlyFee |
number |
1500 |
Fee to release before the set release date |
Police.impound.defaultLateFee |
number |
1000 |
Fee to release after the set release date (normal) |
Police.impound.minFee / maxFee |
number |
0 / 100000 |
Allowed impound fee range |
Police.impound.maxDays |
number |
30 |
Max days in the future for the release date |
Police.billing |
string |
'auto' |
'auto' (tries okokBilling → esx_billing → qb phone_invoices → internal), or force one of 'okokbilling', 'esx_billing', 'qb', 'internal', 'custom' |
Police.society |
table |
{ name = 'police', account = 'society_police', label = 'LSPD' } |
Society info passed to the billing bridge |
Police.customBill |
function | nil |
nil |
Custom billing hook used when Police.billing = 'custom'; return true if billed externally |
IPL interiors — fully in-game, DB-backed
IPL interiors are no longer defined in config (shared/ipls.lua was removed). They live entirely in the aty_garage_ipls table and are created/edited in-game via the IPL editor (/garageipl → dropdown → + Create New Interior). Each record holds its own IPL names to load (ipl_names), interior start point (teleport), menu/enter coordinates, and vehicle spawn slots — all garages sharing an IPL id share those interior points. On first start, known legacy ids (impexp_mid, impexp_large, eclipse_10car, lom_bank) get their ipl_names/teleport seeded automatically if empty.
server/config.lua — Discord logging (server-only, never sent to clients)
| Option |
Type |
Default |
Description |
Config.Logs.enabled |
boolean |
true |
Master switch for Discord webhook logging |
Config.Logs.console |
boolean |
true |
Also print every logged event to the server console |
Config.Logs.botName |
string |
'ATY Garage' |
Webhook embed username |
Config.Logs.webhook |
string |
(set) |
Default webhook URL used when a category has none set |
Config.Logs.webhooks |
table |
(all categories set) |
Per-category webhook override: vehicle, economy, impound, admin |
Config.Logs.colors |
table |
blue/green/red/yellow |
Embed color per category |
Security note: the shipped server/config.lua currently has a live Discord webhook URL filled in for the default and every category. Since this file only lives server-side it isn't leaked to clients, but you should still replace it with your own server's webhook(s) (or blank them out) before going live — the checked-in URL should be treated as compromised.
Exports
This resource does not currently register any of its own exports(...) (client or server). All feature access goes through:
- NUI ⇄ server callbacks via
modules.RegisterCallback / modules.TriggerCallback (see Events below for the full callback list — these are internal to the resource's own UI, not meant to be called from other resources).
- The
aty_garage_v2:refreshHouseGarages server event (see Events) as the one documented integration point for ps-housing.
It consumes exports from its dependencies: exports.aty_lib:getModules()/getUtils()/getShared(), and optionally exports['qb-target'] / exports.ox_target and exports['okokBilling']:CreateCustomInvoice(...).
Events
Public integration hook
| Event |
Side |
Direction |
Description |
aty_garage_v2:refreshHouseGarages |
Server |
Trigger into this resource |
Fire this from ps-housing (or any script) after a house's garage data changes to force an immediate rescan + broadcast, instead of waiting for Housing.rescanInterval |
Internal client/server sync (not intended for external use)
| Event |
Side |
Description |
aty_garage_v2:getGarages, getVehicles, takeVehicle, storeVehicle, transferVehicle, getImpoundGarages, issueFine, beginImpound, impoundVehicle, bringVehicle, getLostVehicles, locateVehicle, retrieveLostVehicle, buyInsurance, cancelInsurance, giveKeys, getKeyHolders, revokeKeys, favoriteVehicle, isAdmin, getAdminData, buyGarage, saveGarage, deleteGarage, getIplList, saveIplCoords, sellGarage, transferOwnedGarage, getManageGarage, ejectVehicle, getPurchaseInfo, saveManageGarage, getGarageMembers, addGarageMember, removeGarageMember |
Server (modules.RegisterCallback) |
NUI-driven request/response callbacks used by the resource's own client code |
aty_garage_v2:client:setGarages |
Client (RegisterNetEvent) |
Server pushes the player's currently visible garage list |
aty_garage_v2:client:openAdmin |
Client (RegisterNetEvent) |
Fired by the server-registered AdminCommand once the framework permission check passes; opens the admin UI |
aty_garage_v2:client:openIplEditor |
Client (RegisterNetEvent) |
Fired by the server-registered IplCommand; opens the IPL editor UI |
aty_garage_v2:client:receiveKeys / aty_garage_v2:client:removeKeys |
Client (RegisterNetEvent) |
Notifies a player they were given/revoked persistent keys to a plate |
aty_garage_v2:vehicleDestroyed |
Server (RegisterNetEvent), triggered by client |
Client reports a park-in-place vehicle exploded, so the server marks it destroyed in aty_garage_vehmeta |
aty_garage_v2:server:syncMileage |
Server (RegisterNetEvent), triggered by client |
Rate-limited (max once/10s, capped at 30 miles/sync) mileage delta sync |
aty_garage_v2:targetReady |
Client (AddEventHandler/TriggerEvent, local only) |
Internal signal that target-mode detection finished, used to register target zones/global-vehicle options |
QBCore:Client:OnJobUpdate, esx:setJob, QBCore:Client:OnGangUpdate, QBCore:Client:OnPlayerLoaded, esx:playerLoaded |
Client (listened to, not emitted) |
Trigger a garage-list refresh on job/gang change or player load |
Commands
| Command |
Side |
Default restriction |
Description |
/garageadmin (Config.AdminCommand) |
Client (registered server-side) |
QBCore perms Config.QBPermission (admin, god) / ESX groups Config.ESXGroup (admin, superadmin) |
Opens the garage creation/admin menu |
/garageipl (Config.IplCommand) |
Client (registered server-side) |
Same as above |
Opens the IPL interior editor directly |
/garageblips (Config.Blip.toggleCommand) |
Client |
None (any player) |
Toggles the player's own garage blip visibility; preference stored in a resource KVP |
/impound |
Client |
None enforced at command level; internally requires Config.Police.jobs |
Opens the impound/fine flow for the vehicle you're in, or the nearest one |
/garagedebug |
Client |
Admin only |
Prints interaction mode, target detection, garage count, and the nearest garage's distance/coords to F8 |
/garageprobe |
Client |
Admin only |
Places a bare test target zone to verify ox_target/qb-target renders options |
/iploc [id] [x y z] |
Client |
Admin only |
Loads an IPL and teleports you to its start point (or a candidate x y z) to verify interior coordinates |
/iplhere |
Client |
Admin only |
Prints your current coords + interior id to F8 (for picking interior points) |
The four diagnostic commands (/garagedebug, /garageprobe, /iploc, /iplhere) are admin-gated: each verifies the caller via the server aty_garage_v2:isAdmin callback (same isAdmin() used everywhere else — ACE Config.AdminAce, framework permissions, or Config.AdminGroups) before running. Non-admins get a "no permission" notification and the command does nothing.
Features
- Multiple garage types: public, private (purchasable/manageable), job fleet, impound, and "lost vehicle" garages.
- Framework-agnostic (ESX / QBCore / Qbox) via
aty_lib, including job-grade and QBCore gang-grade access locks per garage (ESX has no standard gang system, so job is used instead for gang-style access).
- Pre-built IPL interior garages (Import/Export mid & large, Eclipse Blvd 10-car, Underground/Lom Bank) with a dedicated in-game editor for menu/preview/spawn/zone points, a showroom of spawned display vehicles, and world-side vehicle spawn slots.
- Park-in-place persistence: vehicles stay physically parked where stored (frozen/invincible display entity), streamed in/out server-side based on player proximity, and survive restarts via the
aty_garage_parked table.
- Vehicle insurance with configurable policy price/duration, and a "Lost Vehicles" recovery flow for destroyed vehicles (insured claim fee vs. uninsured recovery fee) as well as plain lost (not destroyed, not impounded) vehicles.
- Persistent key sharing (
aty_garage_keys) so another player can take/store a plate without transferring ownership, plus revocation.
- Shared/manageable garage members (
aty_garage_members) for co-owned private garages.
- Dynamic
ps-housing house garages generated from the properties table (never written to aty_garages), periodically rescanned and instantly refreshable via aty_garage_v2:refreshHouseGarages.
ox_target / qb-target support with automatic detection and marker-mode fallback.
- Discord webhook logging split into
vehicle, economy, impound, and admin categories, queued to respect Discord rate limits.
- Vue NUI with category tabs, capacity indicator, and a cinematic vehicle preview camera with optional depth-of-field and a periodically-recaptured frosted blur background.
- Anti-dupe protections: per-plate operation locks (
plateLock) and per-player/action rate limiting (rateOk) around take/store/transfer flows.
- Vehicle mileage tracking with a rate-limited client→server sync.
- Police fine and impound workflow for park-in-place vehicles, with configurable reasons/fees/impound duration and a billing bridge that tries
okokBilling → esx_billing → QBCore phone_invoices → internal collection (or a custom hook).
Troubleshooting
| Problem |
Solution |
| Garages don't appear / menu won't open |
Run /garagedebug to check interaction mode, target detection, and the nearest garage's distance/coords. Confirm aty_lib started before aty_garage_v2 in server.cfg. |
| Target option shows no interaction / no marker |
If Config.Interaction = 'target' but neither ox_target nor qb-target is running, the resource falls back to marker mode automatically — check /garagedebug output for ox=/qb= resource state. Use /garageprobe to test whether the running target resource renders options at all. |
| No IPL interiors in the garage/editor dropdown |
Interiors are DB-backed. Create one in-game: /garageipl → dropdown → + Create New Interior (enter an ID, label, and the IPL names to load, then pick the coordinates). Legacy ids are seeded on first start. |
| IPL teleport lands in an empty void |
The teleport (Interior Start Point) of that IPL record doesn't line up with the interior. Re-pick it in the IPL editor, or use /iploc <id> <x> <y> <z> to test candidate coordinates. |
| House garages missing for a player |
Check Config.Housing.enabled = true, Config.Housing.script = 'ps-housing', that ps-housing is running, and that the properties row has a non-empty garage_data and a matching owner_citizenid. Trigger aty_garage_v2:refreshHouseGarages server-side to force a rescan instead of waiting for Housing.rescanInterval. |
| Fines/impound don't bill externally |
Set Config.Police.billing to a specific mode to isolate the issue, or check that okokBilling/esx_billing is actually started (GetResourceState) or that the phone_invoices table exists for the 'qb' mode. 'auto' silently falls back to internal collection if all external attempts fail. |
| No Discord logs arriving |
Check server/config.lua → Config.Logs.enabled and that webhook/webhooks[category] is a valid URL (replace the shipped example webhook with your own). Run the server-console-only /garagelogtest command to confirm delivery, and check Config.Logs.console output for the resolved settings. |
| Vehicle stuck as "destroyed" / can't retrieve |
Open the Lost Vehicles menu: insured vehicles pay Insurance.claimFee, uninsured ones pay Insurance.uninsuredFee only if Insurance.allowUninsured = true — otherwise the vehicle cannot be recovered by design. |
/iploc, /iplhere, /garagedebug, /garageprobe do nothing for a player |
Working as intended — these diagnostics are admin only (verified via the server aty_garage_v2:isAdmin callback). Grant the player admin (ACE Config.AdminAce, framework admin/god/superadmin, or Config.AdminGroups) to use them. |