lrc-horsethief
A progressive horse theft mission system for RedM. Players take contracts from an NPC, ride out to steal specific horses guarded by enemies, break them in, and deliver them back for a reward. Supports both RSG-Core and VORP-Core out of the box.
Overview
How the resource works from start to finish.
Mission flow
A prompt appears within Config.PromptRange units. Pressing it opens the mission selection menu.
Missions unlock progressively — Mission 1 is always available. Each subsequent mission requires completing the previous one a set number of times (Config.CompletionsToUnlock).
A map blip and GPS route guide the player. When they enter Config.TriggerRadius, enemies and the target horse spawn nearby.
Once mounted, a 15-second calm timer begins. The player must stay on the horse continuously. Getting bucked off resets the timer.
After calming, a delivery blip and GPS route appear. Riding to within Config.DeliveryRadius of the NPC completes the mission and awards cash and/or items.
File structure
Every file in the resource and what it does.
client/ and server/ files and the UI shell are untouched by configuration.Quick start
Minimum steps to get the resource running on your server.
Place lrc-horsethief inside your resources folder and add ensure lrc-horsethief to your server.cfg.
Open config.lua and set Config.Lang to 'en' or 'ro'.
Set the coords inside Config.ContactNPCs in config.lua to a location that suits your server's map.
Open configmissions.lua and check each mission's rewards.cash value against your server's economy. Adjust as needed.
Run ensure lrc-horsethief in your txAdmin console or server console. The NPC will spawn and the missions will be live.
config.lua
The main configuration file. Controls the NPC, map blips, prompts, cooldown, level bonuses, mission area sizes, and enemy loadouts.
Language
Config.Lang = 'en' -- 'en' or 'ro'
Switches all in-game notification text and UI strings between English and Romanian. The locale strings themselves live in locale_init.lua — do not edit that file directly unless you are adding a new language.
Contact NPC — Config.ContactNPCs
Config.ContactNPCs = {
{
model = 'MP_LM_STEALHORSE_BUYERS_01',
coords = vector4(1242.2626, -437.9520, 89.4799, 60.3918),
},
-- add more entries here if needed
}
Each entry in this table spawns one contact NPC in the world. You can have multiple NPCs — each one opens the same mission menu independently. This is useful if you want to place the contact in more than one location on the map.
| Key | Type | Description |
|---|---|---|
| model | string | The ped model hash name to spawn as the contact NPC. |
| coords | vector4 | World position (x, y, z) and heading (w) where the NPC stands. Use a trainer or coords script to find the right position. |
{ model = ..., coords = ... } block, paste it after the first one (separated by a comma), and update the coords.Prompt & proximity — Config.PromptRange
Config.PromptRange = 2.5 -- metres from the NPC before the prompt appears
Config.PromptTitle = 'Horse Thief' -- title shown above the prompt hint
Config.PromptAction = 'Interact' -- label on the prompt button
| Key | Type | Description |
|---|---|---|
| PromptRange | number | Distance in metres. When the player is within this range, the interaction prompt becomes visible and pressable. |
| PromptTitle | string | The header text displayed above the prompt group on screen. |
| PromptAction | string | The label on the prompt button itself (e.g. "Talk", "Interact", "Open"). |
NPC blip — Config.ShowNPCBlip
Config.ShowNPCBlip = false -- set true to show the NPC on the minimap
Config.BlipSprite = 'blip_ambient_horse' -- blip icon hash name
Config.BlipLabel = 'Horse Thief' -- text shown when hovering the blip
Config.BlipScale = 0.35 -- size of the blip icon
When ShowNPCBlip is false, the NPC exists in the world but does not appear on the minimap — players must find them through other means (server lore, signage, etc.). Set it to true to show a minimap marker.
BlipSprite accepts any valid RedM blip hash name. BlipScale controls the size — 0.35 is small and unobtrusive; 0.6–1.0 is more prominent.
Delivery blip — shown during horse return
Config.DeliveryBlipSprite = 'blip_saddle'
Config.DeliveryBlipLabel = 'Deliver Horse'
Config.DeliveryBlipScale = 0.35
Config.DeliveryRadius = 10.0 -- metres from NPC to trigger mission completion
Once a horse is calmed, a delivery blip appears on the NPC's position. DeliveryRadius is the proximity (in metres) within which the mission auto-completes and rewards are paid. Increase it if players are finding the detection too tight; decrease it for a more precise delivery zone.
Mission area radii
Config.CircleRadius = 35.0 -- size of the visual circle drawn on the map
Config.TriggerRadius = 70.0 -- entering this range spawns enemies and the horse
Config.SpawnRadius = 25.0 -- enemies and horse spawn within this range of the center
| Key | Description |
|---|---|
| CircleRadius | The radius of the decorative circle blip shown on the minimap around the mission location. Visual only — does not affect gameplay. |
| TriggerRadius | When the player comes within this many metres of the mission center, the ambush spawns. Should be larger than CircleRadius so the ambush triggers before the player enters the visual circle. |
| SpawnRadius | Enemies and the target horse are scattered randomly within this radius of the mission center point. Keep it smaller than TriggerRadius. |
Enemy models, weapons & accuracy
Config.EnemyModels = {
'g_m_m_uniafricanamericangang_01',
'u_m_m_odriscollbrawler_01',
-- add or remove ped model names here
}
Config.EnemyAccuracy = {
easy = 20, -- 0 = completely blind, 100 = perfect aim
default = 35,
hard = 55,
}
Config.EnemyWeapons = {
easy = {
'weapon_revolver_cattleman',
'weapon_revolver_doubleaction',
},
default = {
'weapon_revolver_cattleman',
'weapon_repeater_winchester',
'weapon_shotgun_doublebarrel',
},
hard = {
'weapon_rifle_springfield',
'weapon_rifle_boltaction',
'weapon_shotgun_semiauto',
},
}
EnemyModels is a shared pool — every spawned enemy picks a random model from this list regardless of mission difficulty. EnemyWeapons and EnemyAccuracy are keyed by difficulty type ('easy', 'default', 'hard'), which is set per-mission inside configmissions.lua.
EnemyModels. Changing weapons per difficulty: add or remove weapon hash names from the relevant difficulty block in EnemyWeapons. One weapon is picked at random per enemy.configmissions.lua
Defines every mission — the target horse, spawn locations, number of enemies, rewards, and the text shown in the mission selection UI.
Full mission structure
[1] = {
type = 'default', -- 'easy' | 'default' | 'hard'
ui = {
image = 'assets/images/m1.png',
label = 'MISSION 1 [Morgan]',
warning = 'This horse is protected by armed gangs!',
details = 'A sturdy Morgan Gelding...\n\nGo steal it.',
},
horselocation = {
vec3(1549.1113, 1073.8922, 181.2135),
vec3(1769.6315, 950.4037, 119.5529), -- optional extra location
},
horse = { model = 'a_c_horse_morgan_bayroan' },
enemies = { count = 8 },
rewards = {
enabled = true,
cash = 50,
-- item = 'horse_deed', -- single item (optional)
-- amount = 1,
-- items = { -- multiple items (optional)
-- { item = 'horse_deed', amount = 1 },
-- { item = 'aged_pirate_rum', amount = 2 },
-- },
},
},
Field reference
| Field | Type | Description |
|---|---|---|
| type | string | Difficulty — controls which weapon pool and accuracy value enemies use. Must be 'easy', 'default', or 'hard' (defined in config.lua). |
| ui.image | string | Relative path to the card image shown in the mission selection menu. Must be a file inside ui/assets/images/. |
| ui.label | string | The mission title shown at the top of the mission card in the UI. |
| ui.warning | string | A short warning line shown below the title. Typically used to warn about enemies. |
| ui.details | string | The mission description paragraph. Use \n\n for paragraph breaks within the string. |
| horselocation | vec3 array | One or more world positions. If more than one is provided, the game picks one at random each time the mission starts — adding variety to the same mission. |
| horse.model | string | The horse ped model to spawn as the target. Must be a valid RedM horse model hash. |
| enemies.count | number | How many enemy peds to spawn when the player enters the trigger radius. |
| rewards.enabled | boolean | Set to false to disable all rewards for this mission entirely. |
| rewards.cash | number | Dollar amount added to the player's cash on successful delivery. Set to 0 or omit for no cash reward. |
| rewards.item / rewards.amount | string / number | A single item name and quantity to give on completion. Uncomment and fill in. |
| rewards.items | table | A list of multiple items, each with item and amount. Use this instead of item/amount when giving more than one item type. |
Adding a new mission
Step-by-step guide to adding Mission 11 (or any subsequent mission) to the resource.
Create or source a horse image and save it as ui/assets/images/m11.png. It must follow the same naming convention — mN.png where N is the mission index.
configmissions.luaScroll to the bottom of Config.Missions = { ... }, after the last entry (e.g. [10] = { ... }).
Copy the block below — it is a ready-to-edit template for Mission 11. Paste it after the closing }, of Mission 10, still inside the outer } of Config.Missions.
Set your type, the UI text, one or more horse locations, the horse model, enemy count, and rewards. Save the file.
Run restart lrc-horsethief in the console. The new mission will appear in-game as the last card in the selection menu, locked until Mission 10 has been completed the required number of times.
-- ═══════════════════════════════════════════════════
[11] = {
type = 'hard', -- 'easy' | 'default' | 'hard'
ui = {
image = 'assets/images/m11.png',
label = 'MISSION 11 [Horse Name]',
warning = 'This horse is potentially protected by armed gangs!',
details = 'Describe the horse and the job here.\n\nSecond paragraph.',
},
horselocation = {
vec3(0.0, 0.0, 0.0), -- replace with real coords
},
horse = { model = 'a_c_horse_YOURMODELHERE' },
enemies = { count = 10 },
rewards = {
enabled = true,
cash = 70,
},
},
Config.CompletionsToUnlock times before Mission 11 becomes available. There is no way to make a mission skip-able without editing that value.ui/assets/images/m11.png must exist before you restart the resource. A missing image will cause the card to display a broken image in the selection UI.Mission images
How to replace or add card images shown in the mission selection menu.
Naming convention
Images are named m1.png through mN.png and stored in ui/assets/images/. Each file maps directly to the mission at the same index — m1.png is shown for [1], m2.png for [2], and so on. The path is set per-mission in the ui.image field inside configmissions.lua, so you can also use a completely different filename or subfolder if you prefer.
Replacing an existing image
Simply replace the file at the same path with your new image. No config change is needed — the path is already declared in the mission entry. Recommended dimensions are roughly 400 × 260 px, landscape orientation.
Adding an image for a new mission
Save your image as ui/assets/images/mN.png where N is the new mission index, then reference it in the ui.image field of your new mission entry. The file is served via fxmanifest's wildcard declaration and does not need to be declared manually.
Cooldown system
Prevents players from repeating the same mission back-to-back. Cooldowns are tracked server-side per player and per mission.
Config.CooldownEnabled = true
Config.CooldownMinutes = 10
How it works
When a player successfully completes a mission, the server records a timestamp for that player + mission combination. If the player tries to start the same mission again before CooldownMinutes has elapsed, they receive a notification telling them how much time remains and the mission does not start.
Cooldowns are tracked in memory on the server — they reset if the server restarts. If you need persistent cooldowns across restarts, that would require a database integration not included in this resource.
Disabling cooldowns
Set Config.CooldownEnabled = false to remove all cooldown restrictions. Players will be able to repeat any mission immediately after completing it.
Adjusting the duration
Change Config.CooldownMinutes to any whole number. A value of 0 with CooldownEnabled = true is the same as disabling — the cooldown expires instantly. Recommended range is 5–30 minutes depending on your server's economy pace.
Mission unlock threshold — Config.CompletionsToUnlock
Config.CompletionsToUnlock = 1 -- completions of mission N needed to unlock mission N+1
This is separate from the cooldown. It controls the progression gate — how many times a player must successfully complete Mission N before Mission N+1 becomes available. The default of 1 means one completion unlocks the next mission. Set it to 3 to require three completions, and so on.
Level system
Players earn experience through total mission completions across all missions. Higher levels receive a cash bonus on top of the base mission reward.
Config.Levels = {
{ threshold = 0, level = 1 }, -- 0–49 total completions → Level 1
{ threshold = 50, level = 2 }, -- 50–99 → Level 2
{ threshold = 100, level = 3 }, -- 100–149 → Level 3
{ threshold = 150, level = 4 }, -- 150–199 → Level 4
{ threshold = 200, level = 5 }, -- 200+ → Level 5
}
Config.CashBonusPerLevel = 5 -- $ added per level above 1
How it works
The server tracks a player's total completions across all missions (not per-mission). When a mission is completed, the player's level is checked against the thresholds in Config.Levels. Their level is the highest entry whose threshold is less than or equal to their total completions.
The cash bonus is calculated as (level - 1) × CashBonusPerLevel. At Level 1 there is no bonus. At Level 2 the bonus is +$5. At Level 5 the bonus is +$20 (on top of whatever the mission's base cash reward is).
Customising levels
You can add more levels, change the thresholds, or raise the bonus amount freely. Each entry needs a threshold (minimum total completions to reach that level) and a level number. Keep the entries in ascending threshold order.
CashBonusPerLevel = 10 and a base mission reward of $50, a Level 3 player would receive $50 + (2 × $10) = $70 on completion.Death fail
Optionally fail the active mission if the player dies at any point during it — from the moment the contract is accepted through to delivery.
Config.EnableDeathFail = true
How it works
When true, a watcher thread starts the moment a mission is approved. It runs for the entire mission lifetime — travel to the area, the ambush phase, calming the horse, and delivery — and triggers a mission failure the instant IsEntityDead returns true for the local player.
On death, the mission is cancelled server-side (same as any other failure path), all spawned enemies and the target horse are cleaned up, and the player receives an 'error' notification.
Disabling
Set Config.EnableDeathFail = false to turn the feature off entirely. No watcher thread is created and dying during a mission has no effect on its state — the mission continues when the player respawns.
| Key | Type | Description |
|---|---|---|
| EnableDeathFail | boolean | When true, player death during any phase of a mission fails it immediately. When false, death is ignored by the mission system. |
Notification system
By default the resource sends notifications through RSG-Core or VORP-Core depending on which framework is running. Both can be swapped out for any other notification script with a few lines in fw_func.lua.
Default behaviour
The resource auto-detects the running framework at startup. On RSG-Core it uses ox_lib if available, otherwise falling back to RSGCore:Notify. On VORP-Core it uses vorp:TipRight. You do not need to configure anything for either of these to work.
| Framework | Notification used |
|---|---|
| RSG-Core + ox_lib | exports['ox_lib']:notify({ ... }) (client) / ox_lib:notify (server event) |
| RSG-Core (no ox_lib) | TriggerEvent('RSGCore:Notify', ...) / TriggerClientEvent('RSGCore:Notify', ...) |
| VORP-Core | TriggerEvent('vorp:TipRight', ...) |
Notification types
Throughout the resource, notifications are called with a ntype argument. The values used are:
'success'— mission completed, horse calmed, delivery confirmed'error'— mission failed, horse killed, cooldown active, already in a mission'primary'/'inform'— neutral information (e.g. mount the horse, return to horse)
When replacing the notification system, map these type strings to whatever your target script expects.
fw_func.lua — swapping the notification script
fw_func.lua is the only file you need to edit to change how notifications, money, and items are handled. The rest of the resource calls LRCfw.notify and LRCfw.notifyLocal exclusively — nothing else needs touching.
Structure overview
The file defines four internal setup functions — one client and one server function for each framework — and then auto-detects which framework is running to call the right one. Each setup function assigns implementations to the LRCfw table.
local function setupClientRSG()
LRCfw.notifyLocal = function(msg, ntype, duration)
-- ← edit this to change client notifications on RSG
end
end
local function setupClientVORP()
LRCfw.notifyLocal = function(msg, ntype, duration)
-- ← edit this to change client notifications on VORP
end
end
local function setupServerRSG()
LRCfw.notify = function(src, msg, ntype, duration)
-- ← edit this to change server→client notifications on RSG
end
end
local function setupServerVORP()
LRCfw.notify = function(src, msg, ntype, duration)
-- ← edit this to change server→client notifications on VORP
end
end
Example — replacing with a custom notification script
Say you use a script called my-notify that exposes a client export exports['my-notify']:Send(msg, type, duration) and a server-side client event 'my-notify:client:show'. Here is how you would swap it in:
LRCfw.notifyLocal = function(msg, ntype, duration)
exports['my-notify']:Send(msg, ntype, duration or 5000)
end
LRCfw.notify = function(src, msg, ntype, duration)
TriggerClientEvent('my-notify:client:show', src, msg, ntype, duration or 5000)
end
Apply the same change to both RSG and VORP setup functions if you want it consistent on both frameworks, or only edit the one that applies to your server.
msg— the notification text stringntype— the notification type ('success','error','primary','inform')duration— display time in milliseconds (e.g.5000= 5 seconds)src— (server only) the player source ID to send the notification to
Other functions in fw_func.lua
Beyond notifications, the file also wires up money and item rewards. These are framework-specific and generally do not need changing unless you use a custom inventory or economy script.
| Function | Side | Description |
|---|---|---|
| LRCfw.notify | Server | Sends a notification to a specific player by source ID. Called by the server after mission completion, cooldown checks, etc. |
| LRCfw.notifyLocal | Client | Sends a local notification to the local player. Called directly on the client for immediate feedback (mount prompt, bucked off, etc.). |
| LRCfw.addMoney | Server | Adds cash to the player's character. Wired to p.Functions.AddMoney on RSG and addCurrency on VORP. |
| LRCfw.addItem | Server | Adds an item to the player's inventory. Wired to rsg-inventory:AddItem on RSG and vorp_inventory:addItem on VORP. |
| LRCfw.getCitizenId | Server | Returns the player's citizen/character ID. Used internally for cooldown and progress tracking. |
| LRCfw.hideHUD / showHUD | Client | Hides and restores the HUD when the mission menu is open. |
LRCfw.notify, LRCfw.notifyLocal, LRCfw.addMoney, or LRCfw.addItem will break the resource silently.Support
Full support is provided for every resource.
What's included
Every purchase comes with full support — installation help, configuration questions, and bug fixes. Join the Discord and open a ticket, and 1973 will get back to you directly.