Law Enforcement

Scully Law Enforcement

An easy-to-use LEO system with everything you need to get your server's law enforcement up and running.


Table of Contents

  1. Features
  2. Requirements
  3. Installation
  4. Permissions
  5. Configuration
  6. Data Files
  7. Customising Content
  8. Keybinds & Commands
  9. Integrations
  10. Exports
  11. State Bags
  12. Locales
  13. Showcases
  14. Support

Features

Departments & Duty

  • Department system using ACE permissions, compatible with Discord-based permission resources.
  • Department-specific duty locations, with the option to let other departments use them.
  • One LEO menu keybind for both duty selection and the main menu.
  • When duty locations are enabled, players must be inside an allowed location to open the duty menu.
  • Players can pick any department they have permission for.
  • Global maximum on-duty player limit.
  • Optional automatic off-duty for inactive/AFK players.
  • Configurable blips showing other on-duty LEOs.
  • Optional duty-hour logging to Google Sheets.
  • Optional Discord webhook logging (duty and actions) with rate-limit handling.

Player Interaction

  • Soft cuff, hard cuff and uncuff, with synced animations and visible handcuff props.
  • Optional movement while hard cuffed, plus configurable controls that stay available while cuffed.
  • Cuff escape skill check (can be disabled) and a cutting pliers weapon for breaking cuffs.
  • Dragging with separate animations for normal, cuffed and dead players.
  • Force-sit, and placing players into / removing them from vehicles.
  • Animated revive, with support for swapping in another medical/revive system's event.
  • Optional quick binds for soft cuff, hard cuff and drag.
  • Tackle system.

Identification, Search & Alerts

  • Hamz CAD integration (source provided so you can adapt it to other systems) for name checks, plate checks, and medical, permit and status information.
  • Weapon search, with an optional cuff requirement, and an export to replace it with your own (e.g. ox_inventory).
  • Gunshot / ShotSpotter alerts with a configurable weapon ignore list.
  • 911 calls and a panic button.
  • Configurable 10-codes and alert messages.
  • BAC testing (optional command and state bags to set BAC).
  • GSR testing, with the ability to wash GSR off in water.

Jail

  • Jail time in minutes or seconds, with a configurable maximum.
  • Optional entrance, exit and mugshot scenes.
  • Configurable prison location, distance limit and spawn locations.
  • Optional jail jobs that reduce remaining time, with cooldown handling.
  • Jail time persists across logins.

Props

  • Configurable prop spawner with synced props and a delete option.
  • Per-prop initial heading offset.
  • Optional sequence spawning, with a maximum count and positional offset.
  • Fully configurable spike strips, not tied to the vanilla model.

Vehicles

  • Department-specific vehicle lists organised into categories.
  • Configurable extras, liveries and vehicle modifications, plus optional full upgrades.
  • Vehicles spawn exactly as configured, so officers don't need to set them up each time.
  • Vehicle deletion.

Loadouts & Outfits

  • Department-specific loadouts in multiple categories, with configurable health, armor, weapons and components.
  • Ballistic shield support.
  • Department-specific outfits in multiple categories, with separate male and female sets.

Other

  • Locale support.
  • Native audio and source audio files included.
  • Optional vanilla police disabling.
  • Left/right menu alignment with persistent menu settings.
  • Resource-side anti-cheat checks for restricted state bags and events.
  • OneSync support.

Requirements

  • A FiveM server with OneSync enabled.
  • Server build set to the latest recommended build.

The resource manifest declares:

dependencies {
    '/server:35245', -- Server artifacts / build number
    '/onesync'
}

Installation

You can watch a video tutorial by our affiliate Zino's HQ here (opens in a new tab). Written instructions are also below:

Notice: The video tutorial is currently outdated but will be updated for the latest version soon.

  1. Download the resource from your FiveM Asset Grants (opens in a new tab).

  2. Extract scully_lawenforcement into your server's resources folder.

  3. Move scully_lawenforcement.cfg out of the resource and place it next to your server.cfg.

  4. Add this to the top of your server.cfg:

    exec scully_lawenforcement.cfg
  5. Start the resource:

    ensure scully_lawenforcement
  6. Set up your ACE permissions.

  7. Restart the server.

Important: The resource intentionally throws an error if scully_lawenforcement.cfg is left inside the resource folder. Copy it next to server.cfg, then delete it from the resource. This exists so the step can't be skipped by accident.


Permissions

Permissions are department-based. Each department's table key in data/departments.lua is its ACE permission:

['lspd'] = {
    label = 'Los Santos Police Department',
    -- ...
}

A player with group.lspd can access the LSPD department.

Option 1: server.cfg

add_ace group.lspd group.lspd allow
add_ace group.bcso group.bcso allow

add_principal identifier.discord:YOUR_DISCORD_ID group.lspd

Replace the identifier and group names as required.

Option 2: Discord roles with scully_perms (optional)

Edit scully_perms/config.lua:

Config = {
    Debug = false,
    Guild = '', -- Your Discord guild ID. (Tutorial: https://github-wiki-see.page/m/manix84/discord_gmod_addon_v2/wiki/Finding-your-Guild-ID-%28Server-ID%29)
    Token = '', -- Your Discord bot token from the Discord developer portal. (Tutorial: https://www.writebots.com/discord-bot-token/)
    Permissions = { -- Department -> role ID(s). Accepts a string or a table.
        ['lspd'] = 'roleid',                         -- Single role
        ['bcso'] = {'roleid', 'roleid2', 'roleid3'}  -- Multiple roles
    }
}

Configuration

Server-wide settings live in scully_lawenforcement.cfg. Content such as departments, vehicles and props is configured in the data/ files instead.

Core

setr scully_lawenforcement:uniqueKvpId "change-me"
setr scully_lawenforcement:locale "en"
setr scully_lawenforcement:enablePermissions "true"
SettingDescription
uniqueKvpIdChange this to a value unique to your server. Used for the resource's local settings.
localeLocale file to load from locales/. See Locales.
enablePermissionsToggles the permission checks.

Feature toggles

All default to "true" unless noted.

SettingDescription
enableJailSystemJail system.
enableJailEnterSceneJail entrance scene.
enableJailExitSceneJail exit scene.
enableJailMugshotSceneJail mugshot scene.
enableJailJobsJail jobs that reduce remaining time.
enableWeaponSearchWeapon search.
enableSearchCuffRequirementRequire the target to be cuffed to search them (default "false").
enableEvidenceEvidence system.
enableGSRGSR testing.
enableBACBAC testing.
enableBACCommandThe command for setting BAC.
enablePropSpawnerProp spawner.
enableVehicleSpawnerVehicle spawner.
enableLoadoutOptionsLoadout menu.
enableOutfitOptionsOutfit menu.
enableGunshotAlertsGunshot / ShotSpotter alerts.
gsrWashTimeTime it takes to wash GSR off in water (default 10000).

Duty

setr scully_lawenforcement:enableDutyLocations "true"
setr scully_lawenforcement:dutyLimit 0
setr scully_lawenforcement:enableDutyBlips "true"
setr scully_lawenforcement:enableForceInactiveOffDuty "false"
SettingDescription
enableDutyLocationsWhen "true", players must be inside a department's duty location to select it. Set to "false" to allow selecting a department from anywhere.
dutyLimitMaximum players on duty at once. 0 disables the limit (e.g. 10 allows 10).
enableDutyBlipsOn-duty LEOs see blips for other on-duty players.
enableForceInactiveOffDutyAutomatically puts inactive/AFK players off duty.

Cuffs

setr scully_lawenforcement:enableCuffProps "true"
setr scully_lawenforcement:cuffClothingIds [7, 41, 25]
setr scully_lawenforcement:enableCuffKeys [1, 2, 38, 47, 245, 249]
setr scully_lawenforcement:enableBreakOutCuffs "true"
setr scully_lawenforcement:enableEscapeCuffs "true"
setr scully_lawenforcement:enableHardCuffMovement "false"
SettingDescription
enableCuffPropsShow visible handcuff props.
cuffClothingIds[drawable ID, male variation ID, female variation ID]
enableCuffKeysControl IDs that remain usable while cuffed.
enableBreakOutCuffsAllow breaking cuffs with cutting pliers.
enableEscapeCuffsCuff escape skill check.
enableHardCuffMovementAllow movement while hard cuffed.

Quick binds

setr scully_lawenforcement:enableQuickBinds "true"
setr scully_lawenforcement:quickBinds ["NUMPAD7", "NUMPAD4", "NUMPAD1"]

The three keys map to, in order: soft cuff, hard cuff, drag (commands quickbind-softcuff, quickbind-hardcuff, quickbind-drag).

Tackle

setr scully_lawenforcement:enableTackle "true"
setr scully_lawenforcement:tackleKeybind "G"

Command: tackle

911 & panic

setr scully_lawenforcement:enable911Command "true"
setr scully_lawenforcement:enablePanicButtonCommand "true"

Alerts go to available on-duty LEOs. Alert text and 10-codes are edited in data/codes.lua.

Jail

setr scully_lawenforcement:jailInMinutes "true"
setr scully_lawenforcement:maxJailTime 999
SettingDescription
jailInMinutes"true" = jail time is entered in minutes; "false" = seconds.
maxJailTimeMaximum sentence, in whichever unit is configured. For example, jailInMinutes "true" with maxJailTime 30 allows up to 30 minutes.

Jail locations, jobs, mugshot configuration and the prison distance limit are in data/jail.lua.

Webhooks

set scully_lawenforcement:webhook:duty "false"
set scully_lawenforcement:webhook:action "false"
set scully_lawenforcement:webhook:jail "false"

Replace "false" with a Discord webhook URL to enable that log.

LogCovers
dutyOn-duty / off-duty activity.
actionLEO actions such as cuffing, dragging, jail and revive.
jailJail-related events.

Revive integration

setr scully_lawenforcement:reviveEvent "scully_lawenforcement:revivePlayer"

To use another medical system, replace the value with its client event:

setr scully_lawenforcement:reviveEvent "your_revive_event_here"

A custom event receives the source of the LEO performing the revive. The resource also includes compatibility for the upcoming medical system.


Data Files

Content configuration lives in the data/ directory:

FilePurpose
blockers.luaVanilla police blocker configuration.
codes.lua10-codes and alert messages.
departments.luaDepartments, permissions, locations and blips.
jail.luaPrison locations, jail jobs and mugshot configuration.
loadouts.luaDepartment loadouts and ballistic shields.
misc.luaCuff-breaking weapons, vehicle seat indexes and wheel definitions.
outfits.luaDepartment clothing and outfit categories.
props.luaSpawnable props, sequences and spike strips.
vehicles.luaDepartment vehicles, categories, extras, liveries and mods.
weapons.luaGunshot-alert exclusions and weapon search labels.

Customising

Departments (data/departments.lua)

Each department can define a label, maximum duty distance, whether other departments may use its duty locations, a department blip, an on-duty player blip, and its duty locations.

['lspd'] = {
    label = 'Los Santos Police Department',
    maxDutyDistance = 10.0,
    allowOtherDepartments = true,
 
    blip = {
        enable = true,
        sprite = 60,
        color = 38,
        scale = 0.8
    },
 
    dutyBlip = {
        sprite = 1,
        color = 57,
        scale = 0.8,
        showCone = true
    },
 
    locations = {
        vec3(440.46, -975.71, 30.68)
    }
}

Vehicles (data/vehicles.lua)

Organised by department → category → vehicle. The category name is what officers see in the vehicle menu.

['lspd'] = {
    ['Patrol'] = {
        ['Vehicle Name'] = {
            model = `vehicle_model`,
            upgrade = true,
            livery = 1,
            mods = {
                [11] = 5
            },
            extras = {1, 2, 5}
        }
    },
    ['Supervisor'] = { ... },
    ['Unmarked'] = { ... }
}
OptionDescription
modelVehicle model.
upgradetrue applies max performance upgrades.
liveryLivery index.
mods[mod type] = mod index.
extrasList of extras to enable.

Outfits (data/outfits.lua)

Organised by department and category. Categories appear as submenus, and male and female clothing are defined separately.

['lspd'] = {
    ['Patrol Uniforms'] = {
        Male = {
            ['Uniform 1'] = {
                -- clothing data
            }
        },
        Female = {
            ['Uniform 1'] = {
                -- clothing data
            }
        }
    }
}

Loadouts (data/loadouts.lua)

Loadouts can define health, armor, weapons, weapon components, ballistic shield behaviour and categories.

['Patrol'] = {
    health = 200,
    armor = 100,
    weapons = {
        {
            weapon = `WEAPON_COMBATPISTOL`,
            components = {
                `COMPONENT_AT_PI_FLSH`
            }
        }
    }
}

Props (data/props.lua)

[`prop_barrier_work05`] = {
    label = 'Barrier',
    headingOffset = 90.0,
    sequence = {
        enable = true,
        max = 4,
        offset = vec3(3.0, 0.0, 0.0)
    }
}
OptionDescription
headingOffsetSpawned heading = player heading + this value. Useful when a model's default orientation doesn't match the placement direction.
sequence.enableAllows placing multiple copies of the prop in one go.
sequence.maxMaximum props in a sequence.
sequence.offsetOffset from the previous prop.

Sequences are handy for barriers, cones, spike strips and similar objects.

Spike strips

Any prop can be a spike strip by adding a stinger definition:

stinger = {
    minDimensions = vec3(-0.11, -1.84, -0.03),
    maxDimensions = vec3(0.12, 1.84, 0.07)
}

The dimensions define the detection area used to work out which tyres are crossing the strip. Only tyres that actually intersect the area are deflated.


Keybinds & Commands

Menu keybind

setr scully_lawenforcement:menuKeybind "F5"
setr scully_lawenforcement:menuKeybindController ""

The menu is registered as leomenu. The same keybind opens the department selection menu while off duty and the main LEO menu while on duty. Players can rebind it in the normal FiveM keybind settings.

Commands

Command names come from the locale file, so they can be renamed.

CommandPurpose
leomenuOpens the LEO menu.
setBACSets the player's BAC value.
breakcuffsUses cutting pliers to break cuffs.
panicSends a panic alert.
911 <message>Sends a 911 call.

Integrations

CAD (Hamz)

setr scully_lawenforcement:enableCad "true"
set scully_lawenforcement:cadUrl "https://example.com/cad"

Supports name checks, plate checks and 911 notifications. The implementation is in server/cad.lua, which is not escrowed, so you can adapt it to another CAD.

Google Sheets duty logging

Duty time can be added automatically to each player's running total in a Google Sheet via a Google Apps Script web app.

Support notice: This is an optional third-party integration. Scully's Development support staff will not help with creating, deploying, configuring or troubleshooting Google Apps Script or Google Sheets. There are plenty of guides online covering Apps Script and deploying web apps.

1. Configure the resource

set scully_lawenforcement:enableGoogleSheet "true"
set scully_lawenforcement:googleSheetIdentifier "discord"
set scully_lawenforcement:googleSheetUrls [
    ["lspd", "https://script.google.com/macros/s/yourid/exec", "Roster", "G", "L"],
    ["bcso", "https://script.google.com/macros/s/yourid/exec", "Roster", "G", "L"]
]
SettingDescription
enableGoogleSheet"false" disables the integration.
googleSheetIdentifierIdentifier type used to find players in the sheet. It must match the values in your identifier column.
googleSheetUrlsOne entry per department (see below).

Each googleSheetUrls entry is:

[department, script URL, sheet name, identifier column, hours column]
PositionExampleMeaning
DepartmentlspdDepartment this configuration applies to.
Script URLhttps://script.google.com/...Deployed Apps Script web app URL.
Sheet nameRosterName of the sheet tab.
Identifier columnGColumn containing the player identifier.
Hours columnLColumn where duty time is stored.

The resource sends the player's identifier and the number of seconds spent on duty.

2. Create your sheet

Create a roster sheet, or make a copy of this template (opens in a new tab).

GL
IdentifierLogged Hours
12345678901234567800:00:00
98765432109876543212:34:56

The identifier column must match the identifier the resource sends. The hours column is updated by the script.

3. Add the script

In your sheet, open Extensions → Apps Script, delete the default code, paste the script below and save.

Apps Script code
function doPost(e) {
    const params = JSON.parse(e.postData.contents);
 
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(params.sheetName);
    const identifierCol = params.identifierColumn;
    const hoursCol = params.hoursColumn;
    const identifier = params.identifier;
    const secondsToAdd = Number(params.seconds);
 
    const data = sheet.getDataRange().getValues();
    let foundRow = -1;
 
    for (let i = 0; i < data.length; i++) {
        if (String(data[i][columnToIndex(identifierCol) - 1]).trim() === String(identifier).trim()) {
            foundRow = i + 1;
            break;
        }
    }
 
    // Stop here if the player isn't on the roster
    if (foundRow === -1) {
        return ContentService.createTextOutput("NOT_FOUND");
    }
 
    const currentValue = sheet.getRange(foundRow, columnToIndex(hoursCol)).getValue();
    let currentSeconds = 0;
 
    if (typeof currentValue === 'number') {
        currentSeconds = currentValue * 86400;
    } else if (typeof currentValue === 'string' && currentValue.includes(':')) {
        const parts = currentValue.split(':').map(Number);
        if (parts.length === 3) {
            currentSeconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
        } else if (parts.length === 2) {
            currentSeconds = parts[0] * 3600 + parts[1] * 60;
        }
    }
 
    const newSeconds = currentSeconds + secondsToAdd;
    const newFormatted = secondsToDuration(newSeconds);
 
    sheet.getRange(foundRow, columnToIndex(hoursCol)).setValue(newFormatted);
 
    return ContentService.createTextOutput("OK");
}
 
function columnToIndex(letter) {
    let column = 0;
    for (let i = 0; i < letter.length; i++) {
        column = column * 26 + (letter.charCodeAt(i) - 64);
    }
    return column;
}
 
function secondsToDuration(totalSeconds) {
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}

4. Deploy as a web app

  1. Click Deploy (top right) and choose New deployment.
  2. Select Web app as the type.
  3. Configure access so the FiveM server can reach it.
  4. Click Deploy and complete Google's authorisation prompts.
  5. Copy the Web app URL, which looks like https://script.google.com/macros/s/XXXXXXXXXXXX/exec, into googleSheetUrls.

Important: Use the deployed Web app URL, not the Apps Script editor URL.


Exports

Exports for integrating with other resources.

Client exports

Actions on the closest player unless stated otherwise:

ExportDescription
checkGSR()Checks for gunshot residue.
checkBAC()Checks BAC.
togglePlayerCuffs(hard)Toggles cuffs. true = hard cuff, false = soft cuff.
softCuffPlayer()Soft cuffs.
hardCuffPlayer()Hard cuffs.
uncuffPlayer()Uncuffs.
revivePlayer()Revives.
togglePlayerDrag()Starts/stops dragging.
forceSitPlayer()Forces the player to sit.
seatPlayerInVehicle()Seats the player in a vehicle.
jailPlayer(time)Jails the player. time uses the configured jail unit.
unjailPlayer(serverId)Unjails a player by server ID.
getJailTime()Returns the local player's current jail time.
exports.scully_lawenforcement:togglePlayerCuffs(true)
exports.scully_lawenforcement:jailPlayer(10)
exports.scully_lawenforcement:unjailPlayer(1)
local jailTime = exports.scully_lawenforcement:getJailTime()

customSearch

Replaces the built-in weapon search, for example to integrate another inventory system.

exports.scully_lawenforcement:customSearch(function(serverId)
    -- Your custom search implementation.
end)

customDrag

Replaces the default drag attachment configuration. Your function receives the drag mode (normal or cuffed) and returns the attachment settings. The dead mode always uses the built-in configuration.

exports.scully_lawenforcement:customDrag(function(mode)
    local modes = {
        ['normal'] = {
            animation = {'switch@trevor@escorted_out', '001215_02_trvs_12_escorted_out_idle_guard2'},
            bone = 11816,
            position = vec3(-0.06, 0.65, 0.0),
            rotation = vec3(0.0, 0.0, 0.0)
        },
        ['cuffed'] = {
            bone = 11816,
            position = vec3(0.20, 0.45, 0.0),
            rotation = vec3(0.0, 0.0, 0.0)
        }
    }
 
    return modes[mode]
end)

Server exports

customCheaterRemoval

Replaces the default removal function used when the resource detects an invalid state bag or restricted event. A good place to implement an auto-ban.

exports.scully_lawenforcement:customCheaterRemoval(function(source, reason)
    DropPlayer(source, reason)
end)

getOnDutyCount

Returns the number of players currently on duty.

local onDutyCount = exports.scully_lawenforcement:getOnDutyCount()

State Bags

Player state bags exposed for integrations:

State bagDescription
isLEOPlayer has access to the LEO system.
LEODeptPlayer's current department (e.g. lspd).
onDutyAsLEOPlayer is on duty.
gsrPlayer has gunshot residue.
BACPlayer's current BAC value.
cuffedCuff state: 'soft', 'hard', or unset.
draggingPlayer is dragging another player.
draggedPlayer is being dragged.
jailedPlayer is jailed.
disableCuffAnimsSet to temporarily disable cuff animations.
local isLEO      = LocalPlayer.state.isLEO
local department = LocalPlayer.state.LEODept
local onDuty     = LocalPlayer.state.onDutyAsLEO
 
local cuffed     = LocalPlayer.state.cuffed
local hardcuffed = cuffed == 'hard'
local softcuffed = cuffed == 'soft'

Temporarily disable cuff animations, then restore them:

LocalPlayer.state:set('disableCuffAnims', true, true)
LocalPlayer.state:set('disableCuffAnims', false, true)

API change: State bag names from older versions have been renamed. Update ondutyonDutyAsLEO and departmentLEODept.


Locales

Locale files live in locales/ and the active one is set with:

setr scully_lawenforcement:locale "en"

To add a language, copy an existing locale file and translate its values. Locale files also contain the command names and descriptions, so commands can be customised there.


Showcases

Notice: Some of these are outdated, will be updated to show the new features and changes soon.

FeatureVideo
Duty locationsWatch (opens in a new tab)
Information check and alignment adjustmentWatch (opens in a new tab)
Player optionsWatch (opens in a new tab)
Revive optionWatch (opens in a new tab)
Drag animation for dead playersWatch (opens in a new tab)
Cutting pliers to break off cuffsWatch (opens in a new tab)
Cuffed player POV with escape promptWatch (opens in a new tab)
BAC commandWatch (opens in a new tab)
Washing off GSRWatch (opens in a new tab)
Jail system, cop POVWatch (opens in a new tab)
Jail system, player POVWatch (opens in a new tab)
Prop spawner, vehicle spawner, loadouts, ballistic shield and outfitsWatch (opens in a new tab)

Support

For support, updates and community discussion, join the Scully's Development community: https://discord.gg/scully (opens in a new tab)