/vRP-1

FiveM (http://fivem.net) RP addon/framework

Primary LanguageLuaMIT LicenseMIT

vRP

vRP

FiveM RP addon/framework
The project aim to create a generic and simple RP framework to prevent everyone from reinventing the wheel.
Contributions are welcomed.

💡
Config examples are git branches as cfg_NAME…​. It’s easier to update, for example if cfg_FR is not up-to-date, you could do a git merge from the last version.

Support me on Patreon to keep this project alive:

Support me and the project on Patreon
See also (and use it as a basis to understand how to develop extensions for vRP):

Resource credits

Features

  • basic admin tools (kick,ban,whitelist), groups/permissions, languages, identification system (persistant user id for database storage), user custom data key/value

  • player state auto saved to database (hunger,thirst,weapons,player apparence,position)

  • player identity/phone/aptitudes (education/exp), emotes, business system / money (wallet/bank), homes

  • cloakrooms (uniform for jobs), basic police (PC, check, I.D., handcuff, jails, seize weapons/items), basic emergency (coma, reanimate)

  • inventory (with custom item definition, parametric items), chests (vehicle trunks), item transformer (harvest, process, produce) (illegal informer)

  • basic implementations: ATM, market, gunshop, skinshop, garage

  • gui (dynamic menu, progress bars, prompt) API, blip, markers (colored circles), areas (enter/leave callbacks) API

  • database SQL/MySQL "driver" system to interface to any MySQL resources

  • proxy for easy inter-resource developement, tunnel for easy server/clients communication

TODO list

  • home stuff (home garage,etc)

  • vehicle customization

  • drop weapon/save weapon components

  • police pc: add custom police records

  • admin: tp to marker

  • blips/markers invisiblity option rework

  • clients<→server scheme for VoIP

  • props, NPC

  • multi-character (CData)

  • multi-server (SData)

  • no more Proxy API, vRP extension with script loading

  • properly stacked menu, list instead of a map, all menus created using builder

  • OOP

NOTES

Homes

⚠️
The home system is experimental, don’t expect too much from it at this point. But it’s a good basis for some RP interactions, and further developments.

How it works

Homes are closed interiors allocated to players when they want to go inside their home, it means that if no slots are availables, you can’t enter to your home. Slots are freed when everyone moves out, die, crash or disconnect inside, the slot could not close itself in rare cases, only "eject all" will close the slot. So it’s possible that all slots are locked after a while, restarting the server will fix the issue.

Also, player addresses are bound to the home cluster name, it means that if you change the cluster configuration name, players will not be able to enter/sell their home anymore. So choose the name well and don’t change it, if you don’t want to deal with this.

💡
Home components allow developers to create things to be added inside homes using the config files. See the home API.

Tutorials

Deployment

Installation

ℹ️
vRP has been tested under Windows and GNU/Linux with Mono 4.8.

Clone the repository or download the master archive and copy the vrp and vrp_mysql directories to your resource folder. Add vrp_mysql then vrp to the loading resource list (first after the basic FiveM resources is better).

Configuration

⚠️
Only the files in the cfg/ directory should be modified. Modifying the vRP core files is highly discouraged (don’t open an issue if it’s about modified core files).

There is only one required file to configure before launching the server, cfg/base.lua, to setup the MySQL database credentials.

There is a lot to configure in vRP, nothing comes preconfigured so everyone can make his unique server.
Everything you need to know is in the configuration files, but if you have troubles configuring, look at the configuration of the vRP LaTest servers above.

Update

A good way to update (bleeding-edge):
  1. use git to clone vRP to create your own version of it, checkout the branch you want, create a branch from it

  2. create a symbolic link (or an update script) to vrp/ in your fxserver resources directory

  3. (repeat) configure, commit your changes, stay updated with the vRP repository, solve conflicts

This way, you will know when config files should be updated and what exactly has been updated.

A more primitive way to update:
  1. save your cfg/ folder somewhere

  2. copy all new files in vrp/

  3. compare your old cfg/ folder with the new one, fill the gaps (one mistake will break everything, take your time)

  4. replace the new cfg/ folder with the old modified cfg/ folder

Issues / Features / Help

⚠️
The issue section is only for bug reports and feature requests. I will close (and ban) issues not related to the core of vRP, to keep the github clean. Don’t submit issues about your own modifications, I will close them without warning.

When submitting an issue, add any information you can find, with all details. Saying that something doesn’t work is useless and will not solve the issue. If you have errors in your console BEFORE the issue happen, everything could be corrupted, so the issue is irrelevant, you should solve all unrelated errors before submitting issues.

ℹ️
For questions, help, discussions around the project, please go instead on the vRP thread of the FiveM forum here: https://forum.fivem.net/t/release-vrp-framework/22894

Events

Base

-- (server) called after identification
AddEventHandler("vRP:playerJoin",function(user_id,source,name,last_login) end)

-- (server) called when the player join again without triggering the vRP:playerLeave event before
-- (used after a client crash for example)
AddEventHandler("vRP:playerRejoin",function(user_id,source,name) end)

-- (server) called when a logged player spawn
AddEventHandler("vRP:playerSpawn", function(user_id, source, first_spawn) end)

-- (server) called when a player leave
AddEventHandler("vRP:playerLeave",function(user_id, source) end)

-- (server) called when a player join a group
-- gtype can be nil
AddEventHandler("vRP:playerJoinGroup", function(user_id, group, gtype) end)

-- (server) called when a player leave a group
-- gtype can be nil
AddEventHandler("vRP:playerLeaveGroup", function(user_id, group, gtype) end)

-- (client) called when the menu pause state change
AddEventHandler("vRP:pauseChange", function(paused) end)

-- (client) called when the vRP NUI is ready
AddEventHandler("vRP:NUIready", function() end)

API

To call the server-side API functions, get the vRP interface.

local Proxy = module("vrp", "lib/Proxy")

vRP = Proxy.getInterface("vRP")

-- ex:
local user_id = vRP.getUserId(source)

You can also do it client-side, the API is the same as the TUNNEL CLIENT APIs.

vRP = Proxy.getInterface("vRP")

-- ex:
vRP.notify("A notification.") -- notify the player

For the client/server tunnel API, the interface is also vRP, see the Tunnel library below.

💡
In the config file callbacks, you can use directly the globals vRP (Proxy) and vRPclient (the tunnel to the clients).

Base

-- PROXY API

-- return map of user_id -> player source
vRP.getUsers()

-- return user id or nil if the source is invalid
vRP.getUserId(source)

-- return source of the user or nil if not connected
vRP.getUserSource(user_id)

-- return the player spawn count (0 = not spawned, 1 = first spawn, ...)
vRP.getSpawns(user_id)

-- set user data (textual data)
vRP.setUData(user_id,key,value)

-- get user data (textual data)
-- return nil if data not found
vRP.getUData(user_id,key)

-- set server data (textual data)
vRP.setSData(key,value)

-- get server data (textual data)
-- return nil if data not found
vRP.getSData(key)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

-- get user id (client-side)
vRP.getUserId()

-- teleport the player to the specified coordinates
vRP.teleport(x,y,z)

-- get the player position
-- return x,y,z
vRP.getPosition()

-- get the player speed
-- return speed
vRP.getSpeed()

-- return false if in exterior, true if inside a building
vRP.isInside()

-- notify the player
vRP.notify(message)

-- notify the player with picture
vRP.notifyPicture(picture, icon_type, title, int, message)
-- notification pictures, see https://wiki.gtanet.work/index.php?title=Notification_Pictures
-- icon_type => 1 = message received, 3 = notification, 4 = no icon, 7 = message sended

-- play a screen effect
-- name, see https://wiki.fivem.net/wiki/Screen_Effects
-- duration: in seconds, if -1, will play until stopScreenEffect is called
vRP.playScreenEffect(name, duration)

-- stop a screen effect
-- name, see https://wiki.fivem.net/wiki/Screen_Effects
vRP.stopScreenEffect(name)



-- FUNCTIONS BELOW ARE EXPERIMENTALS

-- get nearest players (inside the radius)
-- return map of player => distance in meters
vRP.getNearestPlayers(radius)

-- get nearest player (inside the radius)
-- return player or nil
vRP.getNearestPlayer(radius)


-- animations dict/name: see http://docs.ragepluginhook.net/html/62951c37-a440-478c-b389-c471230ddfc5.htm

-- play animation (new version)
-- upper: true, only upper body, false, full animation
-- seq: list of animations as {dict,anim_name,loops} (loops is the number of loops, default 1)
-- looping: if true, will infinitely loop the first element of the sequence until stopAnim is called
vRP.playAnim(upper, seq, looping)

-- stop animation (new version)
-- upper: true, stop the upper animation, false, stop full animations
vRP.stopAnim(upper)

-- SOUND
-- some lists:
-- pastebin.com/A8Ny8AHZ
-- https://wiki.gtanet.work/index.php?title=FrontEndSoundlist

-- play sound at a specific position
vRP.playSpatializedSound(dict,name,x,y,z,range)

-- play sound
vRP.playSound(dict,name)

Group/permission

Group and permissions are a way to limit features to specific players. Each group have a set of permissions defined in cfg/groups.lua. Permissions can be used with most of the vRP modules, giving the ability to create specific garages, item transformers, etc.

Regular permissions

Regular permissions are plain text permissions, they can be added to groups. You can add a - before the permission to negate (even if other groups add the permission, they will be ignored).

Special item permission

You can use a special permission to check for items. Form: #idname.operator, operators to check the amount are greater >, less <, equal ` `.

  • #tacos.>0 → one or more tacos

  • #weed.1 → exactly one weed

Special aptitude permission

You can use a special permission to check for aptitudes.
Form: @group.aptitude.operator, operators to check the level are greater >, less <, equal ` `.

  • @physical.strength.3 → strength level equal to 3

  • @science.chemicals.>4 → chemicals science level greater or equal to 5

Special function permission

Permissions can also be custom functions, registered by vRP.registerPermissionFunction.
Form: !name.param1.param2…​

Here is a list of permission functions defined by vRP:
!not. …​

negation of another permission function (ex !not.is.inside)

!is.inside

check if the player is inside a building (approximation)

!is.invehicle

check if the player is inside a vehicle

API
-- PROXY API

-- return group title
vRP.getGroupTitle(group)

-- add a group to a connected user
vRP.addUserGroup(user_id,group)

-- remove a group from a connected user
vRP.removeUserGroup(user_id,group)

-- check if the user has a specific group
vRP.hasGroup(user_id,group)

-- register a special permission function
-- name: name of the permission -> "!name.[...]"
-- callback(user_id, parts)
--- parts: parts (strings) of the permissions, ex "!name.param1.param2" -> ["name", "param1", "param2"]
--- should return true or false/nil
vRP.registerPermissionFunction(name, callback)

-- check if the user has a specific permission
vRP.hasPermission(user_id, perm)

-- check if the user has a specific list of permissions (all of them)
vRP.hasPermissions(user_id, perms)

-- get user group by group type
-- return group name or an empty string
vRP.getUserGroupByType(user_id,gtype)

-- return list of connected users by group
vRP.getUsersByGroup(group)

-- return list of connected users by permission
vRP.getUsersByPermission(perm)

Survival

Running, walking, being hurt/injured, and just living add hunger and thirst. When the hunger and the thirst are at their maximum level (100%), next hunger/thirst overflow will damage the character by the same amount (ex: when thirsty, don’t run, take a car).

ℹ️
This module disable the basic health regen.

The survival module implement also a coma system. If the health of the player is below the coma threshold, the player is in coma for a specific duration before dying. The health (thus coma) is recorded in the player state. If a player disconnect and reconnect while in coma, he will fall in coma again and die in a few seconds.

-- PROXY API

-- return hunger (0-100)
vRP.getHunger(user_id)

-- return thirst (0-100)
vRP.getThirst(user_id)

vRP.setHunger(user_id,value)

vRP.setThirst(user_id,value)

-- vary hunger value by variation amount (+ to add hunger, - to remove hunger)
vRP.varyHunger(user_id,variation)

-- same as vary hunger
vRP.varyThirst(user_id,variation)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

-- player health variation (+ to heal, - to deal damage)
vRP.varyHealth(variation)

-- get player health
vRP.getHealth()

-- set player health
vRP.setHealth(health)

-- check if the player is in coma
vRP.isInComa()

-- enable/disable spawned player ability to hurt friendly
-- flag: boolean
vRP.setFriendlyFire(flag)

-- enable/disable spawned player ability to be chased/arrested by cops
-- flag: boolean
vRP.setPolice(flag)

Police

-- PROXY API

-- insert a police record for a specific user
--- line: text for one line (can be html)
vRP.insertPoliceRecord(user_id, line)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

-- apply wanted level
-- stars 1-5
vRP.applyWantedLevel(stars)

-- true to enable, false to disable
-- if enabled, will prevent NPC cops to fire at the player
vRP.setCop(flag)

Player state

-- PROXY API

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

-- get player weapons data
-- return table with weapons data, use print(json.encode(result)) to understand the structure
vRP.getWeapons()

-- give weapons
-- weapons: same structure as returned by getWeapons()
-- (optional) clear_before: if true, will remove all the weapons before adding the new ones
vRP.giveWeapons(weapons,clear_before)

-- get player apparence customization data
-- return table with customization data, use print(json.encode(result)) to understand the structure
-- .model or .modelhash define the player model, the indexes define each component as [drawable_id,texture_id,palette_id] array
-- props are referenced using the prefix "p" for the key (p0,p1,p2,p...), -1 = no prop
vRP.getCustomization()

-- set player apparence
-- customization_data: same structure as returned by getCustomization()
vRP.setCustomization(customization_data)

-- set player armour (0-100)
vRP.setArmour(amount)

Identity

The identity module add identity cards with a car registration number (one per identity, all vehicles will have the same registration number).

-- PROXY API

-- get user identity
-- return nil if not found
-- identity keys are the database fields: user_id, name, firstname, age, registration
vRP.getUserIdentity(user_id)

Money

The money is managed with direct SQL queries to prevent most potential value corruptions. The wallet empties itself when respawning (after death).

-- PROXY API

-- get money in wallet
vRP.getMoney(user_id)

-- set money in wallet
vRP.setMoney(user_id,value)

-- try a payment (wallet only)
-- return true or false (debited if true)
vRP.tryPayment(user_id,amount)

-- try full payment (wallet + bank to complete payment)
-- return true or false (debited if true)
vRP.tryFullPayment(user_id,amount)

-- give money to wallet
vRP.giveMoney(user_id,amount)

-- get bank money
vRP.getBankMoney(user_id)

-- set bank money
vRP.setBankMoney(user_id,value)

-- try a withdraw
-- return true or false (withdrawn if true)
vRP.tryWithdraw(user_id,amount)

-- try a deposit
-- return true or false (deposited if true)
vRP.tryDeposit(user_id,amount)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

Inventory

The inventory is autosaved and, as the wallet, gets empty upon death.

Items

Items are simple identifiers associated with a quantity in an inventory. But they can also be parametrics.

Parametrics items are identified like other items in the inventory but also have arguments as: weapon|pistol instead of just an ID. Parametric items don’t contain any data, they are generic item definitions that will be specialized by the arguments.

-- PROXY API

-- define an inventory item (call this at server start) (parametric or plain text data)
-- idname: unique item name
-- name: display name or genfunction
-- description: item description (html) or genfunction
-- choices: menudata choices (see gui api) only as genfunction or nil
-- weight: weight or genfunction
--
-- genfunction are functions returning a correct value as: function(args) return value end
-- where args is a list of {base_idname,arg,arg,arg,...}

vRP.defInventoryItem(idname,name,description,choices,weight)

-- return name, description, weight
vRP.getItemDefinition(idname)

vRP.getItemName(idname)

vRP.getItemDescription(idname)

vRP.getItemChoices(idname)

vRP.getItemWeight(idname)

-- add item to a connected user inventory
vRP.giveInventoryItem(user_id,idname,amount,notify)

-- try to get item from a connected user inventory
-- return true if the item has been found and the quantity removed
vRP.tryGetInventoryItem(user_id,idname,amount,notify)

-- get item amount from a connected user inventory
vRP.getInventoryItemAmount(user_id,idname)

-- get connected user inventory
-- return map of full idname => amount or nil
vRP.getInventory(user_id)

-- clear connected user inventory
vRP.clearInventory(user_id)

-- compute weight of a list of items (in inventory/chest format)
vRP.computeItemsWeight(items)

-- return user inventory total weight
vRP.getInventoryWeight(user_id)

-- return user inventory max weight
vRP.getInventoryMaxWeight(user_id)

-- open a chest by name
-- cb_close(): called when the chest is closed
vRP.openChest(source, name, max_weight, cb_close)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API
Example 1. Full example of a resource defining a water bottle item.

Once defined, items can be used by any resources (ex: they can be added to shops).

local Proxy = module("vrp", "lib/Proxy")
local Tunnel = require("vrp", "lib/Tunnel")

vRP = Proxy.getInterface("vRP")
vRPclient = Tunnel.getInterface("vRP","vrp_waterbottle")

-- create Water bottle item
local wb_choices = {}  -- (see gui API for menudata choices structure)

wb_choices["Drink"] = {function(player,choice) -- add drink action
  local user_id = vRP.getUserId(player) -- get user_id
  if user_id then
    if vRP.tryGetInventoryItem(user_id,"water_bottle",1) then -- try to remove one bottle
      vRP.varyThirst(user_id,-35) -- decrease thirst
      vRPclient.notify(player,"~b~ Drinking.") -- notify
      vRP.closeMenu(player) -- the water bottle is consumed by the action, close the menu
    end
end
end,"Do it."}

-- add item definition
vRP.defInventoryItem("water_bottle","Water bottle","Drink this my friend.",function() return wb_choices end,0.5)

-- (at any time later) give 2 water bottles to a connected user
vRP.giveInventoryItem(user_id,"water_bottle",2)

Item transformer

The item transformer is a very generic way to create harvest and processing areas.

The concept:
  • you can use the action of the item transformer when entering the area

  • the item transformer has a number of work units, regenerated at a specific rate

  • the item transformer takes reagents (money, items or none) to produce products (money or items) and it consumes a work unit

This way, processing and harvesting are limited by the work units.

💡
Item transformers can be dynamically set and removed, if you want to build random harvest points.
-- add an item transformer
-- name: transformer id name
-- itemtr: item transformer definition table
--- name
--- permissions (optional)
--- max_units
--- units_per_minute
--- x,y,z,radius,height (area properties)
--- r,g,b (color)
--- recipes, map of action =>
---- description
---- in_money
---- out_money
---- reagents: items as idname => amount
---- products: items as idname => amount
---- aptitudes: list as "group.aptitude" => exp amount generated
--- onstart(player,recipe): optional callback
--- onstep(player,recipe): optional callback
--- onstop(player,recipe): optional callback
vRP.setItemTransformer(name,itemtr)

-- remove an item transformer
vRP.removeItemTransformer(name)
Example 2. Example from another resource using proxy
local itemtr = {
  name="Water bottles tree", -- menu name
  r=0,g=125,b=255, -- color
  max_units=10,
  units_per_minute=5,
  x=1858,y=3687.5,z=34.26, -- pos
  radius=5, height=1.5, -- area
  recipes = {
    ["Harvest"] = { -- action name
      description="Harvest some water bottles.", -- action description
      in_money=0, -- money taken per unit
      out_money=0, -- money earned per unit
      reagents={}, -- items taken per unit
      products={ -- items given per unit
        ["water_bottle"] = 1
      }
    }
  }
}

vRP.setItemTransformer("my_unique_transformer",itemtr)
ℹ️
For static areas, configure the file cfg/item_transformers.lua, the transformers will be automatically added.

Home

-- PROXY API

-- define home component (oncreate and ondestroy are called for each player entering/leaving a slot)
-- name: unique component id
-- oncreate(owner_id, slot_type, slot_id, cid, config, data, x, y, z, player)
-- ondestroy(owner_id, slot_type, slot_id, cid, config, data, x, y, z, player)
--- owner_id: user_id of house owner
--- slot_type: slot type name
--- slot_id: slot id for a specific type
--- cid: component id (for this slot)
--- config: component config
--- data: component datatable
--- x,y,z: component position
--- player: player joining/leaving the slot
vRP.defHomeComponent(name, oncreate, ondestroy)

-- user access a home by address (without asking)
-- return true on success
vRP.accessHome(user_id, home, number)

-- get players in the specified home slot
-- return map of user_id -> player source or nil if the slot is unavailable
vRP.getHomeSlotPlayers(stype, sid)
Basic components
Chest

chest A home chest.

_config = {
  weight = 200
}
Wardrobe

wardrobe Save your character customization in the wardrobe, so you don’t need to customize/pay clothes in skinshop again.

Game table

gametable Bet with other peoples.

Item transformer

itemtr Set the config as any item transformer structure configuration.

Radio

radio

_config = {
  stations = { -- map of name -> audio source url
    ["station 1"] = "url",
    ...
  },
  position = {x,y,z} -- optional: define a different position for the audio source (placed 1 meter above the component by default)
}

Mission

-- PROXY API

-- start a mission for a player
--- mission_data:
---- name: Mission name
---- steps: ordered list of
----- text
----- position: {x,y,z}
----- onenter(player,area)
----- onleave(player,area) (optional)
----- blipid, blipcolor (optional)
vRP.startMission(player, mission_data)

-- end the current player mission step
vRP.nextMissionStep(player)

-- stop the player mission
vRP.stopMission(player)

-- check if the player has a mission
vRP.hasMission(player)

GUI

Controls for the menu are by default the cellphone controls (LEFT,RIGHT,UP,DOWN,CANCEL,SELECT and OPEN to open the main menu).

🔥
Don’t forget to change the key to open the phone for something different than UP. You can also use the middle mouse button by default.
💡
You can change the controls in cfg/client.lua.
You can customize the GUI css in cfg/gui.lua.
-- PROXY API

-- HOW TO: building a dynamic menu
local menudata = {}
menudata.name = "My Menu"

-- shift menu from the top by 75px and set the menu header to green
menudata.css = {top = "75px", header_color = "rgba(0,255,0,0.75)"} -- exhaustive list

menudata.onclose = function(player)
  print("menu closed")
end

local onchoose = function(player,choice,mod)
  -- mod will be input modulation -1,0,1 (left,(c)enter,right)
  print("player choose "..choice)
  vRP.closeMenu(source) -- close the menu after the first choice (an action menu for example)
end

-- add options and callbacks
menudata["Option1"] = {onchoose, "this <b>option</b> is amazing"} -- callaback and description
menudata["Option two"] = {onchoose} -- no description
menudata["Another option"] = {function(choice) print("another option choice") end,"this<br />one<br />is<br />better"}
-- END HOW TO

-- open a dynamic menu to the client (will close previously opened menus)
vRP.openMenu(source, menudata)

-- close client active menu
vRP.closeMenu(source)

-- prompt textual (and multiline) information from player
-- cb_result: function(player,result)
vRP.prompt(source,title,default_text,cb_result)

-- ask something to a player with a limited amount of time to answer (yes|no request)
-- time: request duration in seconds
-- cb_ok: function(player,ok)
vRP.request(source,text,time,cb_ok)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

-- return menu state
--- opened: boolean
vRP.getMenuState()

-- return menu paused state
vRP.isPaused()

-- progress bar


-- create/update a progress bar
-- anchor: the anchor string type (multiple progress bars can be set for the same anchor)
---- "minimap" => above minimap (will divide that horizontal space)
---- "center" => center of the screen, at the bottom
---- "botright" => bottom right of the screen
vRP.setProgressBar(name,anchor,text,r,g,b,value)

-- set progress bar value in percent
vRP.setProgressBarValue(name,value)

-- set progress bar text
vRP.setProgressBarText(name,text)

-- remove progress bar
vRP.removeProgressBar(name)


-- div

-- dynamic div are used to display formatted data
-- if only some part of the div changes, use JS pre-defined functions to hide/show the div and change the data

-- set a div
-- css: plain global css, the div class is ".div_nameofthediv"
-- content: html content of the div
vRP.setDiv(name,css,content)

-- set the div css
vRP.setDivCss(name,css)

-- set the div content
vRP.setDivContent(name,content)

-- execute js for the div
-- js variables: this is the div
vRP.divExecuteJS(name,js)

-- remove the div
vRP.removeDiv(name)

-- announce

-- add an announce to the queue
-- background: image url (800x150)
-- content: announce html content
vRP.announce(background,content)
Extending menus

Some menus can be built/extended by any resources with menu builders.

List of known menu names you can extend, each line is description (data properties):
main

main menu (player)

police

police menu (player)

admin

admin menu (player)

vehicle

vehicle menu (user_id, player, vname)

phone

phone menu, no properties, builders are called one time after server launch

static:<name>

any static menu, replace <name> by the static menu name (player)

-- PROXY API

-- register a menu builder function
--- name: menu type name
--- builder(add_choices, data) (callback, with custom data table)
---- add_choices(choices) (callback to call once to add the built choices to the menu)
vRP.registerMenuBuilder(name, builder)

-- build a menu
--- name: menu name type
--- data: custom data table
-- return built choices
vRP.buildMenu(name, data)

Audio

-- TUNNEL CLIENT API

-- play audio source (once)
--- url: valid audio HTML url (ex: .ogg/.wav/direct ogg-stream url)
--- volume: 0-1
--- x,y,z: position (omit for unspatialized)
--- max_dist  (omit for unspatialized)
vRP.playAudioSource(url, volume, x, y, z, max_dist)

-- set named audio source (looping)
--- name: source name
--- url: valid audio HTML url (ex: .ogg/.wav/direct ogg-stream url)
--- volume: 0-1
--- x,y,z: position (omit for unspatialized)
--- max_dist  (omit for unspatialized)
vRP.setAudioSource(name, url, volume, x, y, z, max_dist)

-- remove named audio source
vRP.removeAudioSource(name)
Notes
  • it uses the Web Audio API of CEF

  • CEF used by FiveM doesn’t have mp3/m3u support, so only direct links to ogg/vorbis/(maybe opus) stream will work (for radio stream)

  • .wav/.ogg formats are supported

  • there is no optimization for punctual audio sources, they will be added and removed when they end (no cache)

  • punctual audio sources will not play if the player is 2*max_dist far away

  • persistent audio sources will pause themselves when the player is 2*max_dist far away, and play again when inside this radius (save the bandwidth for radio streams or big music files)

VoIP

The VoIP system of vRP is designed using WebRTC and a p2p architecture. It allows to create things like voice chat with spatialization, group radio with audio effects (ex: police radio) or phone calls. It is an experimental feature.

ℹ️
Check cfg/client.lua and cfg/gui.lua to configure the VoIP (to also replace the internal voice chat if wanted). You will need to setup a STUN/TURN server to have WebRTC working properly.
💡
You can use coturn which should be available on most platforms/distributions and is a STUN and TURN server.
Example 3. Basic example
  • launch turnserver: turnserver -a -u user:password -r "myserver"

  • configure iceServers

  • cfg.voip_peer_configuration = {
      iceServers = {
        {
          urls = {"stun:mydomain.ext:3478", "turn:mydomain.ext:3478"},
          username = "user",
          credential = "password"
        }
      }
    }
-- TUNNEL CLIENT API

-- request connection to another player for a specific channel
vRP.connectVoice(channel, player)

-- disconnect from another player for a specific channel
-- player: nil to disconnect from all players
vRP.disconnectVoice(channel, player)

-- register callbacks for a specific channel
--- on_offer(player): should return true to accept the connection
--- on_connect(player, is_origin): is_origin is true if it's the local peer (not an answer)
--- on_disconnect(player)
vRP.registerVoiceCallbacks(channel, on_offer, on_connect, on_disconnect)

-- check if there is an active connection
vRP.isVoiceConnected(channel, player)

-- check if there is a pending connection
vRP.isVoiceConnecting(channel, player)

-- return connections (map of channel => map of player => state (0-1))
vRP.getVoiceChannels()

-- enable/disable speaking
--- player: nil to affect all channel peers
--- active: true/false
vRP.setVoiceState(channel, player, active)

-- configure channel (can only be called once per channel)
--- config:
---- effects: map of name => true/options
----- spatialization => { max_dist: ..., rolloff: ..., dist_model: ... } (per peer effect)
----- biquad => { frequency: ..., Q: ..., type: ..., detune: ..., gain: ...} see WebAudioAPI BiquadFilter
------ freq = 1700, Q = 3, type = "bandpass" (idea for radio effect)
----- gain => { gain: ... }
vRP.configureVoice(channel, config)

Map

-- PROXY API

-- create/update a player area (will trigger enter and leave callbacks)
-- cb_enter, cb_leave: function(player,area_name)
vRP.setArea(source,name,x,y,z,radius,height,cb_enter,cb_leave)

-- check if a player is in an area
vRP.inArea(source,name)

-- remove a player area
vRP.removeArea(source,name)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

-- set the GPS destination marker coordinates
vRP.setGPS(x,y)

-- set route to native blip id
vRP.setBlipRoute(id)

-- create new blip, return native id
vRP.addBlip(x,y,z,idtype,idcolor,text)

-- remove blip by native id
vRP.removeBlip(id)

-- set a named blip (same as addBlip but for a unique name, add or update)
-- return native id
vRP.setNamedBlip(name,x,y,z,idtype,idcolor,text)

-- remove a named blip
vRP.removeNamedBlip(name)

-- add a circular marker to the game map
-- return marker id
vRP.addMarker(x,y,z,sx,sy,sz,r,g,b,a,visible_distance)

-- remove marker
vRP.removeMarker(id)

-- set a named marker (same as addMarker but for a unique name, add or update)
-- return id
vRP.setNamedMarker(name,x,y,z,sx,sy,sz,r,g,b,a,visible_distance)

-- remove a named marker
vRP.removeNamedMarker(name)

Misc

-- PROXY API

-- remove the player uniform (cloakroom)
vRP.removeCloak(player)

-- TUNNEL SERVER API

-- TUNNEL CLIENT API

Libs

utils

lib/utils defines global tools required by vRP and vRP extensions.

-- load a lua resource file as module
-- rsc: resource name
-- path: lua file path without extension
module(rsc, path)

-- create an async returner (require a Citizen thread) (also alias for Citizen.CreateThreadNow)
-- return returner (r:wait(), r(...))
async()

-- CLIENT and SERVER globals
-- booleans to known the side of the script
⚠️
Any function making usage of async() require a Citizen thread if not already in one. Citizen will throw an error if you’re not in one.

Proxy

The proxy lib is used to call other resources functions through a proxy event.

resource1.lua
local Proxy = module("vrp", "lib/Proxy")

Resource1 = {}
Proxy.addInterface("resource1",Resource1) -- add functions to resource1 interface (can be called multiple times if multiple files declare different functions for the same interface)

function Resource1.test(a,b)
  print("resource1 TEST "..a..","..b)
  return a+b,a*b -- return two values
end
resource2.lua
local Proxy = module("vrp", "lib/Proxy")

Resource1 = Proxy.getInterface("resource1")

local rvalue1, rvalue2 = Resource1.test(13,42)
print("resource2 TEST rvalues = "..rvalue1..","..rvalue2)

The notation is Interface.function(…​).

💡
Good practice is to get the interface once and set it as a global, but if you want to get multiple times the same interface from the same resource, you need to specify a unique identifier (the name of the resource + a unique id for each one).

Tunnel

The idea behind tunnels is to easily access any declared server function from any client resource, and to access any declared client function from any server resource.

Example 4. Example of two-way resource communication
Server-side myrsc:
local Tunnel = module("vrp", "lib/Tunnel")

-- build the server-side interface
serverdef = {} -- you can add function to serverdef later in other server scripts
Tunnel.bindInterface("myrsc",serverdef)

function serverdef.test(msg)
  print("msg "..msg.." received from "..source)
  return 42
end

-- get the client-side access
clientaccess = Tunnel.getInterface("myrsc")

-- (later, in a player spawn event) teleport the player to 0,0,0
clientaccess.teleport(source,0,0,0)
Client-side myrsc:
-- build the client-side interface
clientdef = {} -- you can add function to clientdef later in other client scripts
Tunnel.bindInterface("myrsc",clientdef)

function clientdef.teleport(x,y,z)
  SetEntityCoords(GetPlayerPed(-1), x, y, z, 1,0,0,0)
end

-- sometimes, you would want to return the tunnel call with asynchronous data
-- ex:
function clientdef.setModel(hash)
  local r = async()

  Citizen.CreateThread(function()
    -- do the asynchronous model loading
    Citizen.Wait(1000)

    r(true)  -- return true
  end)

  return r:wait() -- wait for the async returned value
end

-- get the server-side access
serveraccess = Tunnel.getInterface("myrsc")

-- call test on server and print the returned value (in an async context)
local r = serveraccess.test("my client message")
print(r) -- true

Now if we want to use the same teleport function in another resource:

local Tunnel = module("lib/Tunnel")

-- get the client-side access of myrsc
myrsc_access = Tunnel.getInterface("myrsc","myotherrsc")

-- (later, in a player spawn event) teleport the player to 0,0,0
myrsc_access.teleport(source,0,0,0)

This way resources can easily use other resources client/server API.

The notation is Interface.function(dest, …​).

💡
Good practice is to get the interface once and set it as a global, but if you want to get multiple times the same interface from the same resource, you need to specify a unique identifier (the name of the resource + a unique id for each one).
ℹ️
Tunnel and Proxy are blocking calls in the current coroutine until the values are returned, to bypass this behaviour, especially for the Tunnel to optimize speed (ping latency of each call), use as prefix for the function name (Proxy/Tunnel interfaces should not have functions starting with ). This will discard the returned values, but if you still need them, you can make normal calls in a new Citizen thread with Citizen.CreateThreadNow or async to have non-blocking code.
⚠️
Also remember that Citizen event handlers (used by Proxy and Tunnel) seem to not work while loading the resource, to use the Proxy at loading time, you will need to delay it with Citizen.CreateThread or a SetTimeout.

Database

SQL queries are managed by DB drivers, you can use the default vRP driver vrp_mysql or use a custom one (vrp_mysql has crappy code, see alternatives).

DB drivers will register themselves (as resources) with a specific name to use in cfg/base.lua. Since there is no guarantee about when the driver will be registered, all queries will be cached until that moment.

ℹ️
[vRP] DB driver "driver_name" not initialized yet (X prepares cached, Y queries cached). is not an error, but a warning that the driver is not registered yet and will stop being outputted if the driver is loaded (a message will also say that the driver is loaded).
-- API (PROXY)

-- register a DB driver
--- name: unique name for the driver
--- on_init(cfg): called when the driver is initialized (connection), should return true on success
---- cfg: db config
--- on_prepare(name, query): should prepare the query (@param notation)
--- on_query(name, params, mode): should execute the prepared query
---- params: map of parameters
---- mode:
----- "query": should return rows (list of map of parameter => value), affected
----- "execute": should return affected
----- "scalar": should return a scalar
vRP.registerDBDriver(name, on_init, on_prepare, on_query)

-- prepare a query
--- name: unique name for the query
--- query: SQL string with @params notation
vRP.prepare(name, query)

-- execute a query
--- name: unique name of the query
--- params: map of parameters
--- mode: default is "query"
---- "query": should return rows (list of map of field => value), affected
---- "execute": should return affected
---- "scalar": should return a scalar
vRP.query(name, params, mode)

-- shortcut for vRP.query with "execute"
vRP.execute(name, params)

-- shortcut for vRP.query with "scalar"
vRP.scalar(name, params)
Example 5. Usage
-- execute the command after a while, get all banned users
local rows, affected = vRP.query("vRP/myrsc_getbans", {banned = true}) -- in async context
-- rows: rows as a list
-- affected: number of rows affected (when updating things, etc)
-- display banned users

-- execute the command after a while, get all non banned users
local rows, affected = vRP.query("vRP/myrsc_getbans", {banned = false}) -- in async context
-- rows: rows as a list
-- affected: number of rows affected (when updating things, etc)
-- display banned users