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
- Features
- Requirements
- Installation
- Permissions
- Configuration
- Data Files
- Customising Content
- Keybinds & Commands
- Integrations
- Exports
- State Bags
- Locales
- Showcases
- 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.
-
Download the resource from your FiveM Asset Grants (opens in a new tab).
-
Extract
scully_lawenforcementinto your server'sresourcesfolder. -
Move
scully_lawenforcement.cfgout of the resource and place it next to yourserver.cfg. -
Add this to the top of your
server.cfg:exec scully_lawenforcement.cfg -
Start the resource:
ensure scully_lawenforcement -
Set up your ACE permissions.
-
Restart the server.
Important: The resource intentionally throws an error if
scully_lawenforcement.cfgis left inside the resource folder. Copy it next toserver.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.lspdReplace 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"| Setting | Description |
|---|---|
uniqueKvpId | Change this to a value unique to your server. Used for the resource's local settings. |
locale | Locale file to load from locales/. See Locales. |
enablePermissions | Toggles the permission checks. |
Feature toggles
All default to "true" unless noted.
| Setting | Description |
|---|---|
enableJailSystem | Jail system. |
enableJailEnterScene | Jail entrance scene. |
enableJailExitScene | Jail exit scene. |
enableJailMugshotScene | Jail mugshot scene. |
enableJailJobs | Jail jobs that reduce remaining time. |
enableWeaponSearch | Weapon search. |
enableSearchCuffRequirement | Require the target to be cuffed to search them (default "false"). |
enableEvidence | Evidence system. |
enableGSR | GSR testing. |
enableBAC | BAC testing. |
enableBACCommand | The command for setting BAC. |
enablePropSpawner | Prop spawner. |
enableVehicleSpawner | Vehicle spawner. |
enableLoadoutOptions | Loadout menu. |
enableOutfitOptions | Outfit menu. |
enableGunshotAlerts | Gunshot / ShotSpotter alerts. |
gsrWashTime | Time 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"| Setting | Description |
|---|---|
enableDutyLocations | When "true", players must be inside a department's duty location to select it. Set to "false" to allow selecting a department from anywhere. |
dutyLimit | Maximum players on duty at once. 0 disables the limit (e.g. 10 allows 10). |
enableDutyBlips | On-duty LEOs see blips for other on-duty players. |
enableForceInactiveOffDuty | Automatically 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"| Setting | Description |
|---|---|
enableCuffProps | Show visible handcuff props. |
cuffClothingIds | [drawable ID, male variation ID, female variation ID] |
enableCuffKeys | Control IDs that remain usable while cuffed. |
enableBreakOutCuffs | Allow breaking cuffs with cutting pliers. |
enableEscapeCuffs | Cuff escape skill check. |
enableHardCuffMovement | Allow 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| Setting | Description |
|---|---|
jailInMinutes | "true" = jail time is entered in minutes; "false" = seconds. |
maxJailTime | Maximum 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.
| Log | Covers |
|---|---|
duty | On-duty / off-duty activity. |
action | LEO actions such as cuffing, dragging, jail and revive. |
jail | Jail-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:
| File | Purpose |
|---|---|
blockers.lua | Vanilla police blocker configuration. |
codes.lua | 10-codes and alert messages. |
departments.lua | Departments, permissions, locations and blips. |
jail.lua | Prison locations, jail jobs and mugshot configuration. |
loadouts.lua | Department loadouts and ballistic shields. |
misc.lua | Cuff-breaking weapons, vehicle seat indexes and wheel definitions. |
outfits.lua | Department clothing and outfit categories. |
props.lua | Spawnable props, sequences and spike strips. |
vehicles.lua | Department vehicles, categories, extras, liveries and mods. |
weapons.lua | Gunshot-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'] = { ... }
}| Option | Description |
|---|---|
model | Vehicle model. |
upgrade | true applies max performance upgrades. |
livery | Livery index. |
mods | [mod type] = mod index. |
extras | List 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)
}
}| Option | Description |
|---|---|
headingOffset | Spawned heading = player heading + this value. Useful when a model's default orientation doesn't match the placement direction. |
sequence.enable | Allows placing multiple copies of the prop in one go. |
sequence.max | Maximum props in a sequence. |
sequence.offset | Offset 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.
| Command | Purpose |
|---|---|
leomenu | Opens the LEO menu. |
setBAC | Sets the player's BAC value. |
breakcuffs | Uses cutting pliers to break cuffs. |
panic | Sends 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"]
]| Setting | Description |
|---|---|
enableGoogleSheet | "false" disables the integration. |
googleSheetIdentifier | Identifier type used to find players in the sheet. It must match the values in your identifier column. |
googleSheetUrls | One entry per department (see below). |
Each googleSheetUrls entry is:
[department, script URL, sheet name, identifier column, hours column]| Position | Example | Meaning |
|---|---|---|
| Department | lspd | Department this configuration applies to. |
| Script URL | https://script.google.com/... | Deployed Apps Script web app URL. |
| Sheet name | Roster | Name of the sheet tab. |
| Identifier column | G | Column containing the player identifier. |
| Hours column | L | Column 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).
| G | L |
|---|---|
| Identifier | Logged Hours |
123456789012345678 | 00:00:00 |
987654321098765432 | 12: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
- Click Deploy (top right) and choose New deployment.
- Select Web app as the type.
- Configure access so the FiveM server can reach it.
- Click Deploy and complete Google's authorisation prompts.
- Copy the Web app URL, which looks like
https://script.google.com/macros/s/XXXXXXXXXXXX/exec, intogoogleSheetUrls.
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:
| Export | Description |
|---|---|
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 bag | Description |
|---|---|
isLEO | Player has access to the LEO system. |
LEODept | Player's current department (e.g. lspd). |
onDutyAsLEO | Player is on duty. |
gsr | Player has gunshot residue. |
BAC | Player's current BAC value. |
cuffed | Cuff state: 'soft', 'hard', or unset. |
dragging | Player is dragging another player. |
dragged | Player is being dragged. |
jailed | Player is jailed. |
disableCuffAnims | Set 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
onduty→onDutyAsLEOanddepartment→LEODept.
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.
| Feature | Video |
|---|---|
| Duty locations | Watch (opens in a new tab) |
| Information check and alignment adjustment | Watch (opens in a new tab) |
| Player options | Watch (opens in a new tab) |
| Revive option | Watch (opens in a new tab) |
| Drag animation for dead players | Watch (opens in a new tab) |
| Cutting pliers to break off cuffs | Watch (opens in a new tab) |
| Cuffed player POV with escape prompt | Watch (opens in a new tab) |
| BAC command | Watch (opens in a new tab) |
| Washing off GSR | Watch (opens in a new tab) |
| Jail system, cop POV | Watch (opens in a new tab) |
| Jail system, player POV | Watch (opens in a new tab) |
| Prop spawner, vehicle spawner, loadouts, ballistic shield and outfits | Watch (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)