Developer

5 min readUpdated Today

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

open/server.lua
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)
5end

Fires once when a placeable turret has been persisted to the DB and spawned in the world.

Hook_OnTurretDestroyed

open/server.lua
1---@param turretId number
2---@param attackerSrc number|nil  src that dealt the killing blow, nil for environmental deaths
3function Hook_OnTurretDestroyed(turretId, attackerSrc)
4end

Fires 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

open/server.lua
1---@param src number
2---@param turretId number
3---@param healthBefore number
4---@param healthAfter number
5function Hook_OnTurretRepaired(src, turretId, healthBefore, healthAfter)
6end

Fires after a repair item has been consumed and t.health updated.

Hook_OnAmmoLoaded

open/server.lua
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)
6end

Hook_BeforeFixedSpawn

open/server.lua
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
5end

Called 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

open/client.lua
1---@param turret table   local turret wrapper (id, data, config, props)
2function Hook_OnLocalTurretPlaced(turret)
3end

Fires on every client in scope when a new turret appears locally.

Hook_OnLocalTurretDestroyed

open/client.lua
1---@param turret table
2function Hook_OnLocalTurretDestroyed(turret)
3end

Hook_OnEnterManualControl / Hook_OnExitManualControl

open/client.lua
1---@param turret table
2function Hook_OnEnterManualControl(turret)
3end
4
5function Hook_OnExitManualControl(turret)
6end

Fires 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:

open/shared.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

lua
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

lua
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.

lua
1/turret_revive paleto_police_roof

Gated 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

lua
1Database_DeleteTurret(turretId)

Deletes a single turret row by id. Used internally by permanentDestruction and dismantle flows.

Database_DeleteFixedByConfigId

lua
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

lua
1Database_ReviveFixedByConfigId(fixedConfigId)

Clears is_destroyed = 0 and sanitises health on the row. The underlying operation of /turret_revive.

Database_MarkFixedDestroyed

lua
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 scope
  • ml_turret:client:turretRemoved: broadcasts a turret removal (dismantle, permanent destruction)
  • ml_turret:client:turretUpdated: partial update to lt.data (groupMembers changes, etc.) scoped to clients within 150m
  • ml_turret:client:openPanel: sends the tactical UI payload to one player
  • ml_turret:client:updatePanel: live panel refresh after an action
  • ml_turret:client:receiveGroupInvite: delivers an invite to the target player

Client → Server

  • ml_turret:server:placeTurret: fires after the placement progress bar completes
  • ml_turret:server:enterControl / ml_turret:server:exitControl
  • ml_turret:server:loadAmmo / ml_turret:server:unloadAmmo
  • ml_turret:server:refuel / ml_turret:server:repair
  • ml_turret:server:dismantleTurret / ml_turret:server:rebuildTurret
  • ml_turret:server:inviteGroupMember / ml_turret:server:acceptGroupInvite
  • ml_turret:server:removeGroupMember
  • ml_turret:server:toggleAI
  • ml_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

open/server.lua
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)
8end
open/shared.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 on the turret type:

shared/config.lua
1acceptedCalibers = { '12g_buckshot', '762x39' },
2defaultCaliber   = '12g_buckshot',
open/server.lua
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