Home Scripts Docs Customers Contact
All documentation DD Plug System

Build a pack,
set it running.

Setup, permissions and configuration for dd-plugsystem, written alongside the code and shipped with every download.

A plug drop management system for FiveM. Staff build plug packs, loot tables of items and cash, assign them to specific players and set an interval. The server then delivers that pack to that player automatically, on that schedule, forever. Everything runs from a React panel gated behind FiveM ACE permissions.

Requirements

RequirementNotes
FiveM server (artifact 6683+)Lua 5.4 enabled
oxmysqlRequired. The only hard dependency
ESX, QBCore or QboxAuto-detected
ox_inventory or qb-inventoryAuto-detected
ox_lib (optional)Notifications, the text prompt and the pickup progress bar
ox_target or qb-target (optional)Third-eye interaction on the package. Auto-detected

Installation

  1. Place the folder in your resources directory:
    resources/[custom]/dd-plugsystem
    Keep the folder name dd-plugsystem. If you rename it, update the ensure line to match; nothing else needs changing.
  2. The database is optional. The resource creates its own tables on first start. To run it yourself instead:
    mysql -u root -p your_database < sql/digitalden.plugsystem.sql
    Four tables are created: dd_plug_packs, dd_plug_assignments, dd_plug_drops, dd_plug_logs.
  3. Add it to server.cfg, after your framework, your inventory and oxmysql:
    ensure oxmysql
    ensure es_extended        # or qb-core / qbx_core
    ensure ox_inventory       # or qb-inventory
    ensure dd-plugsystem
  4. Grant yourself access with the ACE lines in the next section, then restart.

Verifying it started

[dd-plugsystem] Framework detected: qbox
[dd-plugsystem] Inventory detected: ox
[dd-plugsystem] Database schema verified.
[dd-plugsystem] Loaded 0 pack(s).
[dd-plugsystem] Loaded 0 assignment(s).
[dd-plugsystem] Scheduler online.

Join the server and run /plugpanel.

ACE permissions

This resource uses FiveM ACE permissions only. It does not read your framework's admin groups, so it behaves identically on ESX, QBCore and Qbox.

The four nodes

NodeGrants
digitalden.plugsystem.viewOpen the panel; read packs, assignments, drops and logs.
digitalden.plugsystem.assignEverything in view, plus create, edit, pause and delete assignments.
digitalden.plugsystem.manageEverything in assign, plus create, edit and delete packs, force a drop, reload the item list.
digitalden.plugsystem.adminEverything in manage, plus prune logs and use /plugreload.

The tiers are inclusive. Granting admin alone gives full access, so there is no need to grant the lower nodes as well.

Grant the nodes to your groups

# Full access
add_ace group.admin digitalden.plugsystem.admin allow

# Can build packs and force drops, cannot prune logs
add_ace group.moderator digitalden.plugsystem.manage allow

# Can only assign packs to players
add_ace group.helper digitalden.plugsystem.assign allow

# Read-only access to the panel
add_ace group.trialstaff digitalden.plugsystem.view allow

Put your staff into those groups

Any identifier FiveM understands works: license:, steam:, discord:, fivem:, xbl:, live:. The license identifier is the most reliable. Find a player's license by running status in the server console while they are connected.

add_principal identifier.license:0123456789abcdef0123456789abcdef01234567 group.admin
add_principal identifier.discord:123456789012345678 group.moderator
add_principal identifier.fivem:1234567 group.helper

ACE changes are read at server start. Restart the server, not just the resource.

One person, without a group

add_ace identifier.license:0123456789abcdef... digitalden.plugsystem.manage allow

Denying access

deny always beats allow, which is how you revoke a node from somebody inside a group:

add_ace identifier.license:0123456789abcdef... digitalden.plugsystem.admin deny

Renaming the nodes

If your server uses a different convention, change them in config/config.lua:

Config.Permissions = {
    view    = 'myserver.plugs.view',
    manage  = 'myserver.plugs.manage',
    assign  = 'myserver.plugs.assign',
    admin   = 'myserver.plugs.admin'
}

If you are denied in game you get a notification, and the server console prints the exact node that was required. That line tells you precisely what to add.

Framework and inventory

Everything lives in config/config.lua, which is escrow-exempt and fully editable after purchase.

Config.Framework = 'auto'   -- 'auto' | 'esx' | 'qb' | 'qbox'
Config.Inventory = 'auto'   -- 'auto' | 'ox' | 'qb'

Auto-detection checks for qbx_core, es_extended then qb-core, in that order. Set it explicitly if you run more than one side by side during a migration.

Resource names are configurable too, for a renamed core:

Config.FrameworkResources = { esx = 'es_extended', qb = 'qb-core', qbox = 'qbx_core' }
Config.InventoryResources = { ox = 'ox_inventory', qb = 'qb-inventory' }

Delivery modes

Config.Delivery.mode = 'dropoff'          -- 'dropoff' | 'direct'
Config.Delivery.pickupTimeout = 0         -- minutes; 0 = until the next restart
  • dropoff spawns a package at a drop point, gives the player a blip and GPS route, and they press E to collect it.
  • direct puts the items straight into their inventory with a notification. Anything they cannot carry is reported back and recorded in the log.

pickupTimeout = 0, the default, means a package stays exactly where it landed for the whole session. Because a restart despawns every prop, anything still uncollected then is written off on the next boot and logged as expired. Set a number of minutes instead if you want packages to time out during the session.

Drop points

Where a package lands is decided in three levels, most specific first:

  1. The assignment's pin, set in the assignment editor. Applies to that one player.
  2. The pack's pin, set in the pack editor. Applies to every drop of that pack.
  3. Config.DropLocations, the shared pool, picked at random and scattered within Config.DropRadius.

Both pins are captured with a Pin here button that copies the coordinates you are standing on, so you never have to read them off a map. Clear a pin with the bin icon to fall back to the next level.

Keep the radius small. Config.DropRadius defaults to 12.0, and a large radius is the usual reason packages end up inside buildings, on roofs or in walls. A 45 m scatter will leave open ground almost anywhere in the city; 10 to 15 m is the sweet spot.

To add your own points, stand where you want the drop and run /plugdrophere. The exact line to paste into Config.DropLocations is printed to the server console, which is far more reliable than reading coordinates off a map. The client also probes for real ground at spawn time, so a slightly wrong Z still settles the package on the surface rather than under it.

Warnings and scheduler

Config.Warnings = {
    enabled = true,
    minutesBefore = 5,
    duration = 7000
}

One banner per whole minute remaining, starting minutesBefore out and counting down to the last. Warnings only fire for players who are online, and the banner hides itself while the panel is open so the two never overlap.

Config.Scheduler = {
    tickSeconds = 15,             -- how often due assignments are checked
    deliverOnLoginDelay = 15,     -- seconds after login before a missed order is handed over
    catchUpMissed = true,         -- skip forward over intervals missed while the server was down
    catchUpMaxCycles = 3
}

Packs, cash and icons

Config.Packs = {
    maxItemsPerPack = 25,
    allowCashReward = true,
    cashAccount = 'money',             -- ESX: money  | QB/Qbox: mapped to cash
    dirtyMoneyAccount = 'black_money', -- ESX: black_money | QB/Qbox: mapped to crypto
    maxCashReward = 250000
}

Account names are translated per framework automatically, so those two values are correct on every supported core.

Item icons

The pack editor shows a visual grid of every registered item with its real in-game icon. Paths resolve per inventory:

Config.ItemImages = {
    enabled = true,
    ox = 'nui://%s/web/images/%s',   -- resource name, filename
    qb = 'nui://%s/html/images/%s'
}

Set enabled = false to fall back to text placeholders. ox_inventory items that declare their own client.image are honoured, as are qb-inventory items with a custom image field.

Blacklisting items

Config.Blacklist.items = { 'weapon_rpg', 'armour' }

Blacklisted items cannot be added to a pack, and the check runs on the server, not in the UI.

Notifications

Config.Notify.provider = 'auto'   -- 'auto' | 'ox_lib' | 'mythic' | 'framework' | 'custom'

auto prefers ox_lib, then mythic_notify, then your framework's own. For a custom system set provider = 'custom' and point Config.Notify.resource at your event name; it fires as TriggerEvent(resource, message, kind, title).

Anti-exploit

Config.AntiExploit = {
    maxNuiCallsPerMinute = 120,
    requirePermissionOnEveryCall = true,
    logDenied = true
}

Every request is permission-checked server-side regardless of what the UI shows, and denied attempts are logged with the player's name and server id.

Interaction and pickup

Config.Interaction = {
    mode   = 'auto',     -- 'auto' | 'target' | 'textui' | 'keypress'
    target = 'auto',     -- 'auto' | 'ox_target' | 'qb-target'
    textui = 'auto',     -- 'auto' | 'ox_lib' | 'native'
    label  = 'Collect package',
    icon   = 'fa-solid fa-box-open',
    key    = 38          -- E
}
  • textui, the shipped default, draws an on-screen prompt and listens for Config.Interaction.key.
  • auto uses a third-eye if ox_target or qb-target is running, and falls back to a text prompt.
  • target forces third-eye. With no target resource running it warns once and falls back, rather than leaving the package uncollectable.
  • keypress is the bare native prompt with no dependency at all.

The prompt style is set by textui: nui is a built-in prompt in the panel's style with no dependencies, ox_lib uses ox_lib's own text UI, native uses the game's help box. Move the built-in one with promptPosition: top-left, top-center, left-center or bottom-center.

Config.Collect = {
    duration = 4500,
    useProgressBar = true,
    cancellable = false,
    pickupAnim = { dict = 'pickup_object', clip = 'pickup_low' },
    workAnim   = { dict = 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', clip = 'machinic_loop_mechandplayer' }
}

Nothing is attached to the player; the only prop is the package on the ground. With ox_lib present the animation and progress bar are driven by its progressBar, so an interrupted pickup does not consume the drop.

Using the panel

Open with /plugpanel, or bind a key by setting Config.Keybind.enabled = true.

Plug packs

Name, colour and description, an optional cash range, then the loot table. Items come from a searchable grid of everything on your server shown with its real inventory icon, so there are no spawn codes to type. Each item gets a minimum, a maximum and a chance. Every item is rolled independently, and if every roll fails one random entry is granted at its minimum, so a drop is never empty. Disabling a pack stops deliveries without deleting its assignments.

Assignments

Pick a pack, a player and an interval from 1 minute to 14 days. Online players are listed first; typing in the search also queries your framework's player table, so offline characters can be assigned too.

  • Max deliveries deactivates the assignment after that many drops. 0 is unlimited.
  • Pickup location pins this player's drops to where you are standing, overriding the pack's own location.
  • Deliver first drop now fires once on save, then follows the interval.

How the countdown behaves

next_run is an absolute timestamp, so a countdown survives a restart exactly where it left off and runs on real time whether or not the player is online. Pausing banks the time left and resuming hands it straight back. Editing a name, pack or player never disturbs a running countdown; only changing the interval, or ticking deliver first drop now, reschedules.

The player has to be there

When a drop fires and the player is on the server, it lands. If not, it is recorded as a missed order: nothing spawns, and the order stays claimable until their next scheduled drop. Join inside that window and it is handed over shortly after loading in. Let the window pass and it is gone.

Nothing ever stacks up. At most one claimable order per assignment exists at a time, and forcing a drop to an offline player is refused rather than queued. /mydrops tells the player plainly where they stand.

Forcing a drop restarts that assignment's cycle. The countdown resets to the full interval from that moment and the delivery counts toward any cap, so a player never gets a forced drop and a scheduled one back to back.

The scheduler sleeps until whichever assignment is due first rather than polling on a fixed beat, so a drop lands on its countdown even at 1-minute intervals. Config.Scheduler.tickSeconds is only the longest it will ever sleep.

Activity and System

Activity is a filterable audit trail of every action and delivery, with actor, target and relative timestamps. Admins can prune entries older than 30 days. System is a diagnostics readout: detected framework and inventory, delivery mode, scheduler tick, pickup window, drop location count, resource version, and the four ACE nodes with the ones you hold marked.

Commands

CommandPermissionDescription
/plugpanelviewOpen the management panel.
/plugcollectnoneCollect the package you are standing on, if the E prompt is not usable.
/mydropsnonePlayer tracker: every pack assigned to them, a live countdown, and whether a package is waiting.
/plugdropheremanagePrints your position to the console as a ready-to-paste Config.DropLocations line.
/plugdebugadminDumps what the scheduler sees: tick health, every countdown, and why any of them is not firing. Start here if a drop has not landed.
/plugreloadadminReload packs, assignments and the item list without restarting.

Command names are configurable via Config.Command.

Exports and logging

Server exports

-- Returns every configured pack
local packs = exports['dd-plugsystem']:GetPacks()

-- Returns every assignment
local assignments = exports['dd-plugsystem']:GetAssignments()

-- Deliver a pack to a player right now, bypassing the schedule
local ok, mode = exports['dd-plugsystem']:GiveDrop('char1:1100001abcdef', 2)

-- Check a player's access level ('view' | 'assign' | 'manage' | 'admin')
local allowed = exports['dd-plugsystem']:HasPlugAccess(source, 'manage')

GiveDrop returns true plus the delivery mode used (dropoff, direct or queued), or false plus a reason.

Discord logging

Config.Logs = {
    enabled = true,
    webhook = 'https://discord.com/api/webhooks/...',
    botName = 'DigitalDEN',
    avatar  = '',
    color   = 3066993,
    events  = {
        packCreated = true,
        dropCollected = true,
        permissionDenied = true,
        -- set any event to false to stop sending it
    }
}

Embeds are queued and flushed on a timer so a burst of activity cannot rate-limit your webhook. Database logging keeps working with the webhook left empty.

Database

TablePurpose
dd_plug_packsPack definitions. The loot table is stored as JSON in items.
dd_plug_assignmentsWho receives which pack, the interval, next run timestamp, delivery counters and expiry. Deleting a pack cascades to its assignments.
dd_plug_dropsEvery drop with its rolled contents, state (pending, ready, collected, expired) and timestamps.
dd_plug_logsThe audit trail behind the Activity tab.

All timestamps are Unix epoch seconds.

Troubleshooting

"No supported framework detected"

Your core started after this resource, or it is named something unexpected. Move ensure dd-plugsystem below your core in server.cfg, or set Config.Framework and Config.FrameworkResources explicitly.

"No supported inventory detected"

Same cause. Set Config.Inventory to 'ox' or 'qb' and confirm the resource name in Config.InventoryResources.

The item selector is empty

The item list is cached for five minutes. Press the refresh icon inside the pack editor, or run /plugreload. On qb-inventory the list comes from QBCore.Shared.Items, so a broken items file shows up here first.

Item icons show two-letter placeholders

That item has no matching image file in your inventory resource. ox_inventory reads ox_inventory/web/images/<item>.png, qb-inventory reads qb-inventory/html/images/<image>. If your images live elsewhere, point Config.ItemImages at them. The placeholder is cosmetic; the item still works.

/plugpanel says I have no permission

The ACE node is missing. The server console denial line names the exact node required. ACE changes need a server restart, not a resource restart.

A player never receives their drop

Check the Activity tab. Drop delivered means the server sent it; Order missed means the player was not on the server when it fired. A missed order is handed over Config.Scheduler.deliverOnLoginDelay seconds after they next load in, as long as their next scheduled drop has not already come round.

Items are missing after collecting a package

Their inventory was full. The system reports this to the player and records the failed items in the log entry, but it does not drop them on the floor.

Still stuck?

Run /plugdebug first, then open a ticket with its output and your console lines from resource start. Support is 11:00 to 23:00 UK, every day.

Open a ticket