Developer
Overview
ml_turret does not expose FiveM exports(). All integration happens through hook functions in the open/ folder (safe to edit, not escrowed) and through optional Bridge.OnServerRequest callbacks for custom UI. Internally the script uses Bridge.* from ml_bridge for every framework call, so no raw ESX/QBCore/Qbox reference appears in runtime code.
Server Hooks
Located in open/server.lua. Every hook receives enough context to run custom logic (XP, logging, integrations) without patching the main files.
Hook_OnTurretPlaced
1---@param src number player source
2---@param turret table runtime turret (id, turretType, owner, coords, ...)
3function Hook_OnTurretPlaced(src, turret)
4 -- exports.ml_skills:AddXp('defense', 50, src)
5endFires once when a placeable turret has been persisted to the DB and spawned in the world.
Hook_OnTurretDestroyed
1---@param turretId number
2---@param attackerSrc number|nil src that dealt the killing blow, nil for environmental deaths
3function Hook_OnTurretDestroyed(turretId, attackerSrc)
4endFires on the same tick the turret reaches 0 HP. The rubble prop is spawned 1.5 seconds later; use this hook for instant side-effects (alarms, bounty payouts, dispatch calls).
Hook_OnTurretRepaired
1---@param src number
2---@param turretId number
3---@param healthBefore number
4---@param healthAfter number
5function Hook_OnTurretRepaired(src, turretId, healthBefore, healthAfter)
6endFires after a repair item has been consumed and t.health updated.
Hook_OnAmmoLoaded
1---@param src number
2---@param turretId number
3---@param caliberKey string key from Config.Ammo
4---@param qty number items consumed
5function Hook_OnAmmoLoaded(src, turretId, caliberKey, qty)
6endHook_BeforeFixedSpawn
1---@param fixedDef table entry from Config.FixedTurrets
2---@return boolean|nil return false to cancel the spawn
3function Hook_BeforeFixedSpawn(fixedDef)
4 return true
5endCalled once per fixed turret at resource start, before SpawnTurretEntity. Return false to skip spawning this instance (e.g. based on a daily event or admin toggle).
Client Hooks
Located in open/client.lua.
Hook_OnLocalTurretPlaced
1---@param turret table local turret wrapper (id, data, config, props)
2function Hook_OnLocalTurretPlaced(turret)
3endFires on every client in scope when a new turret appears locally.
Hook_OnLocalTurretDestroyed
1---@param turret table
2function Hook_OnLocalTurretDestroyed(turret)
3endHook_OnEnterManualControl / Hook_OnExitManualControl
1---@param turret table
2function Hook_OnEnterManualControl(turret)
3end
4
5function Hook_OnExitManualControl(turret)
6endFires on the client that takes the gunner seat, at start and end of manual control.
Shared Hooks
open/shared.lua runs on both client and server. Use it to register custom calibres without touching shared/config.lua:
1Config.Ammo['12g_buckshot'] = {
2 label = 'ammo_12g_buck_label',
3 item = 'ammo_12g_buckshot',
4 weaponHash = `WEAPON_PUMPSHOTGUN`,
5 damage = 28.0,
6 amountPerItem = 8,
7 reloadTime = 3500,
8}Then declare acceptedCalibers = { '12g_buckshot' } on a turret type.
Server Callbacks
Exposed via Bridge.OnServerRequest for client-side integrations.
ml_turret:getMyIdentity
1local bundle = Bridge.RequestServer('ml_turret:getMyIdentity')
2-- returns { ownerKey = string, citizenid = string|nil, license = string }Returns the caller's identity bundle. ownerKey is the primary id used in ml_turrets.owner (citizenid on QBCore/Qbox, license on ESX/standalone). Useful when custom UI needs to check ownership client-side.
ml_turret:getNearbyInvitable
1local players = Bridge.RequestServer('ml_turret:getNearbyInvitable', turretId)
2-- returns { { id = serverId, name = displayName }, ... }Returns nearby players who can be invited to this turret's group: within Config.Ownership.nearbyPlayerRange, not the owner, not already a member, not the caller. Owner-only, non-owners get an empty array.
Admin Commands
/turret_revive <fixed_config_id>
Clears the is_destroyed flag on a fixed turret's DB row. Used with stayDestroyed = true to bring a permanently-dead instance back after admin intervention.
1/turret_revive paleto_police_roofGated by Permissions.IsStaff (ACE or fallback identifier). Console invocation always allowed. Restart the resource for the revived turret to respawn.
Debug Commands
Available only when Config.Debug = true (or the caller has the ACE defined in Config.Server.admin.acePermission).
/turret_compose <type>: Spawns the full composition (base + gunner + barrel) with XYZ axes 3m in front of the player
/turret_compose_clear: Removes the active composition
/turret_muzzles <type>: Interactive muzzle-offset editor. TAB cycles muzzles, WASD/QE moves, ENTER exports
/turret_camera <type>: Manual-control camera offset editor
/turret_firetest <type>: Fires 20 test volleys from each muzzle to verify positions
/turret_inspect: Live menu for the nearest turret (HP, ammo, calibre, fuel, target, ...)
/turret_aim_viz: Renders the aim direction + AI target in 3D
/turret_ai_diag: Full AI state diagnostic for the nearest turret
Database Helpers
Server-side globals, primarily for advanced integrations.
Database_DeleteTurret
1Database_DeleteTurret(turretId)Deletes a single turret row by id. Used internally by permanentDestruction and dismantle flows.
Database_DeleteFixedByConfigId
1Database_DeleteFixedByConfigId(fixedConfigId)Deletes the row for a fixed turret by its fixed_config_id. Called when refillOnRestart = true to reset state at boot.
Database_ReviveFixedByConfigId
1Database_ReviveFixedByConfigId(fixedConfigId)Clears is_destroyed = 0 and sanitises health on the row. The underlying operation of /turret_revive.
Database_MarkFixedDestroyed
1Database_MarkFixedDestroyed(t)Persists is_destroyed = 1 immediately for a fixed turret. Called on permanentDestruction death so the next boot's BuildTurretFromFixed sees the destroyed state and applies stayDestroyed logic.
Events
Server → Client
ml_turret:client:turretPlaced: broadcasts a new turret to every client in scopeml_turret:client:turretRemoved: broadcasts a turret removal (dismantle, permanent destruction)ml_turret:client:turretUpdated: partial update tolt.data(groupMembers changes, etc.) scoped to clients within 150mml_turret:client:openPanel: sends the tactical UI payload to one playerml_turret:client:updatePanel: live panel refresh after an actionml_turret:client:receiveGroupInvite: delivers an invite to the target player
Client → Server
ml_turret:server:placeTurret: fires after the placement progress bar completesml_turret:server:enterControl/ml_turret:server:exitControlml_turret:server:loadAmmo/ml_turret:server:unloadAmmoml_turret:server:refuel/ml_turret:server:repairml_turret:server:dismantleTurret/ml_turret:server:rebuildTurretml_turret:server:inviteGroupMember/ml_turret:server:acceptGroupInviteml_turret:server:removeGroupMemberml_turret:server:toggleAIml_turret:server:closePanel
All client→server events are rate-limited and server-validated (zero trust). Forged calls raise security flags logged to the exploits webhook channel.
Examples
1function Hook_OnTurretDestroyed(turretId, attackerSrc)
2 if not attackerSrc then return end
3 exports.ml_skills:AddXp('personal', 25, attackerSrc)
4end
5
6function Hook_OnTurretPlaced(src, turret)
7 exports.ml_skills:AddXp('personal', 50, src)
8end1Config.Ammo['12g_buckshot'] = {
2 label = 'ammo_12g_buck_label',
3 item = 'ammo_12g_buckshot',
4 weaponHash = `WEAPON_PUMPSHOTGUN`,
5 damage = 28.0,
6 amountPerItem = 8,
7 reloadTime = 3500,
8}Then on the turret type:
1acceptedCalibers = { '12g_buckshot', '762x39' },
2defaultCaliber = '12g_buckshot',1function Hook_BeforeFixedSpawn(fixedDef)
2 if fixedDef.id == 'city_centre_defense' then
3 local day = os.date('*t').wday
4 if day == 1 or day == 7 then return false end
5 end
6 return true
7end