🛶 Boats
Documentation related to the jo_boats script.
jo_boats provides a persistent boat ownership system with shops, previews, multiple payment options, customization, storage, damage and repairs, anchoring, docking, and support for carriable and large boats.
1. Installation
jo_boats works with all frameworks supported by the jo_libs framework bridge.
The script requires:
jo_libsversion2.14.2or neweroxmysql
To install jo_boats:
- Download the latest
jo_libsrelease. - Unzip it and place the
jo_libsfolder in your resources folder. - Download
jo_boatsfrom your account. - Unzip it and place the
jo_boatsfolder in your resources folder. - Start the resources in this order in your
server.cfg:
ensure jo_libs
ensure jo_boatsNo SQL file required
The script automatically creates the jo_boats database table on its first startup.
The Boats script is now ready to use.
2. Usage
Players buy and manage their boats from configured boat shops. Purchased boats, their names, customizations, health, storage, last garage, and anchor position are persisted by the server.
Default controls
| Key | Context | Action |
|---|---|---|
ENTER | Shop | Open the shop, confirm a purchase, edit a stored boat |
ENTER | World | Dock, carry, or drop a boat |
E | Boat list / large boat | Spawn and drive, board, or use the safe exit |
B | Priced menu | Switch between payment options |
R | Driving | Anchor or raise the anchor |
O | Boat | Open storage |
Persistence
Purchased boats and their customizations are always persisted. Damage is saved when the authoritative client removes the world entity. If Config.persistAnchored = true, anchored positions are persisted and the script can respawn those boats when a player comes within range.
3. Configuration
The configuration file is config.lua in the resource root. Edit this file to change the settings of the script, then restart the resource.
Keep a copy of your changes before updating the script, as an update replaces this file.
Customization price priority
Customization prices are resolved from the most specific entry to the most general one:
Config.modelPrices[category]["model:index"]Config.modelPrices[category]["model"]Config.prices.customs[category]
For example, Config.modelPrices["lanterns"]["rowboat:2"] only changes the price of lantern index 2 for the rowboat model.
Use the Price Generator to build compatible money, gold, role-currency, item, combination, and alternative-payment price tables.
Blip override priority
Config.blips.shop and Config.blips.boat define global defaults. Individual shops and boat models can override enabled, name, sprite, or color with their own blip table. Disabling a global category disables all blips in that category.
4. FAQ
Why can't I spawn my boat?
Check the status displayed in Manage your boats. A boat cannot spawn if it is already out, is wrecked, the player reached Config.maxSpawnByPlayer, or another vehicle is within Config.distanceSafeSpawn of the spawn point. When Config.bindBoatToGarage = true, also make sure you are visiting the shop where the boat was last docked.
Why is my boat missing from this shop?
When Config.bindBoatToGarage = true, each shop only lists boats whose stored shopId matches that shop. Visit the shop where the boat was purchased or last docked. Set the option to false to list all owned boats at every shop.
Why can't I open the storage?
The model must have a storage table and a valid storagePosition. When Config.restrictStorageToOwner = true, only the owner can open it. Opening storage while driving additionally requires the driver seat and canOpenStorageWhileDriving to be enabled for that model.
What happens when a boat is wrecked?
When Config.canBoatsBeDamaged = true, a destroyed boat is marked as wrecked and removed from the world. Its anchor is cleared, and it cannot be spawned again until its owner repairs it from Manage your boats. Repair cost is calculated from the missing body, engine, and petrol-tank health supported by the model.
Do anchored boats remain after logout or restart?
With Config.persistAnchored = true, anchored boats are not despawned by the logout cleanup and their locations remain in the database. If the world entity is missing, the script attempts to respawn it when a player is within 200 meters. With the option disabled, anchored boats are treated like other spawned boats during logout cleanup.
Why is a customization category missing?
The edit menu only displays categories supported by the model's customization data. Extras are detected from the model dynamically. A model without compatible propsets, liveries, tints, lanterns, or extras will not show that category.
Do I need to import an SQL file?
No. jo_boats creates its database table automatically when oxmysql is ready.
5. For developers
jo_boats exposes Actions and Filters through the hook system. The callbacks and network events used internally by the resource are not part of the public API.
Actions
Actions run after a script operation or client interaction. Their callbacks do not return a value.
Server boatAnchored
Triggered after a boat is anchored or its anchor is raised.
When an anchored boat is wrecked, this action is also triggered with anchored = nil and shouldAnchor = false before boatWrecked.
-- @param source - integer server ID of the owner
-- @param boat - Boat updated boat instance
-- @param anchored - vector4|nil saved anchor position
-- @param shouldAnchor - boolean true when anchoring, false when raising the anchor
exports.jo_boats:registerAction("boatAnchored", function(source, boat, anchored, shouldAnchor)
-- Your code here
end)Server boatBequeathed
Triggered after ownership has been transferred to another connected player.
-- @param source - integer server ID of the previous owner
-- @param boat - Boat transferred boat instance with its updated owner
-- @param targetId - integer server ID of the new owner
exports.jo_boats:registerAction("boatBequeathed", function(source, boat, targetId)
-- Your code here
end)Server boatBought
Triggered after a boat has been paid for, created, passed through the boatBought filter, and persisted.
-- @param source - integer server ID of the buyer
-- @param boat - Boat purchased boat instance
-- @param price - PriceClass price paid by the buyer
-- @param boatData - table purchase data received from the client
exports.jo_boats:registerAction("boatBought", function(source, boat, price, boatData)
-- Your code here
end)Action and filter with the same name
boatBought also exists as a filter. The filter runs first and can replace the boat instance passed to this action.
Client boatCarried
Triggered after the local player successfully attaches a carriable boat.
-- @param boat - Boat carried boat instance
-- @param entity - integer boat entity handle
exports.jo_boats:registerAction("boatCarried", function(boat, entity)
-- Your code here
end)Server boatDeleted
Triggered after a boat has been permanently removed from persistence and synchronized to clients.
-- @param source - integer server ID of the owner
-- @param boatId - integer deleted boat ID
exports.jo_boats:registerAction("boatDeleted", function(source, boatId)
-- Your code here
end)Server boatDespawned
Triggered when a tracked boat entity is removed. If it was removed near a shop, its stored shop ID has already been updated.
This action does not receive a player source because entity removal may happen without a player operation.
-- @param boat - Boat despawned boat instance
-- @param nearestShop - table|nil nearest shop when the entity was removed
exports.jo_boats:registerAction("boatDespawned", function(boat, nearestShop)
-- Your code here
end)Server boatDocked
Triggered after a docking request is accepted and the boat entity is scheduled for removal.
-- @param source - integer server ID of the owner
-- @param boat - Boat docked boat instance
-- @param shopId - string destination shop ID
exports.jo_boats:registerAction("boatDocked", function(source, boat, shopId)
-- Your code here
end)Client boatDropped
Triggered after the local player drops a carried boat that still has a valid world entity.
-- @param boat - Boat dropped boat instance
-- @param entity - integer boat entity handle
exports.jo_boats:registerAction("boatDropped", function(boat, entity)
-- Your code here
end)Server boatHealthSaved
Triggered after the authoritative client health values have been validated and persisted.
-- @param source - integer server ID of the authoritative client
-- @param boat - Boat updated boat instance
-- @param engineHealth - number reported engine health
-- @param petrolTankHealth - number reported petrol-tank health
-- @param bodyHealth - number reported body health
exports.jo_boats:registerAction("boatHealthSaved", function(source, boat, engineHealth, petrolTankHealth, bodyHealth)
-- Your code here
end)Server boatRenamed
Triggered after a boat name has been filtered, saved, and synchronized.
-- @param source - integer server ID of the owner
-- @param boat - Boat renamed boat instance
-- @param name - string final saved name
exports.jo_boats:registerAction("boatRenamed", function(source, boat, name)
-- Your code here
end)Server boatRepaired
Triggered after a boat has been paid for and restored to full supported health.
-- @param source - integer server ID of the owner
-- @param boat - Boat repaired boat instance
-- @param price - PriceClass price paid for the repair
exports.jo_boats:registerAction("boatRepaired", function(source, boat, price)
-- Your code here
end)Server boatSpawned
Triggered after the client-created boat entity has replicated and been linked to its server boat instance.
-- @param source - integer server ID of the spawning client
-- @param boat - Boat spawned boat instance
-- @param netId - integer network ID of the boat entity
exports.jo_boats:registerAction("boatSpawned", function(source, boat, netId)
-- Your code here
end)Server boatStorageOpened
Triggered after the boat inventory has been created and opened for a player.
-- @param source - integer server ID of the player
-- @param boat - Boat boat whose storage was opened
-- @param inventoryId - string inventory ID in the form "jo_boats_<boatId>"
exports.jo_boats:registerAction("boatStorageOpened", function(source, boat, inventoryId)
-- Your code here
end)Server boatWrecked
Triggered after a wrecked boat has been marked for despawn and its anchor has been cleared.
-- @param source - integer server ID of the authoritative client
-- @param boat - Boat wrecked boat instance
-- @param wasAnchored - boolean whether the boat was anchored before being wrecked
exports.jo_boats:registerAction("boatWrecked", function(source, boat, wasAnchored)
-- Your code here
end)Server customizationBought
Triggered after a customization has been applied and persisted. Removing a customization also triggers this action.
-- @param source - integer server ID of the owner
-- @param boat - Boat customized boat instance
-- @param category - string "tints", "propsets", "liveries", "lanterns", or "extras"
-- @param value - number/hash selected customization value
exports.jo_boats:registerAction("customizationBought", function(source, boat, category, value)
-- Your code here
end)Server playerEnteredBoat
Triggered when a player enters a tracked boat. A seat change inside the same boat does not trigger it again.
-- @param source - integer server ID of the player
-- @param boatId - integer entered boat ID
-- @param isDriver - boolean whether the player entered as driver
exports.jo_boats:registerAction("playerEnteredBoat", function(source, boatId, isDriver)
-- Your code here
end)Server playerLeftBoat
Triggered when a player leaves a tracked boat or moves directly to a different tracked boat.
-- @param source - integer server ID of the player
-- @param previousBoatId - integer boat ID the player left
exports.jo_boats:registerAction("playerLeftBoat", function(source, previousBoatId)
-- Your code here
end)Client shopOpened
Triggered after the local player opens a boat shop.
-- @param shop - table opened shop configuration
exports.jo_boats:registerAction("shopOpened", function(shop)
-- Your code here
end)Client shopVendorCameraStarted
Triggered when the camera starts moving to the close-up of the shop seller.
-- @param shop - table opened shop configuration
-- @param cam - number camera handle
exports.jo_boats:registerAction("shopVendorCameraStarted", function(shop, cam)
-- Your code here
end)Client shopVendorCameraStopped
Triggered when the shop menu closes and the camera returns to the gameplay camera.
exports.jo_boats:registerAction("shopVendorCameraStopped", function()
-- Your code here
end)Filters
Filters synchronously control permissions or replace values used by the script. Always return the value expected by the filter.
Server boatBought
Replaces the boat instance immediately after it is created and before the boatBought action and callback response.
-- @param boat - Boat newly created boat instance
-- @param source - integer server ID of the buyer
-- @param boatData - table purchase data received from the client
-- @return Boat boat instance passed to the action and callback response
exports.jo_boats:registerFilter("boatBought", function(boat, source, boatData)
return boat
end)Server canAnchorBoat
Controls whether an owner can change a spawned boat's anchor state.
-- @param canAnchor - boolean result of the built-in validation
-- @param source - integer server ID of the owner
-- @param boat - Boat|nil boat instance
-- @param shouldAnchor - boolean requested anchor state
-- @param anchored - vector4|nil proposed anchor position
exports.jo_boats:registerFilter("canAnchorBoat", function(canAnchor, source, boat, shouldAnchor, anchored)
return canAnchor
end)Server canBequeathBoat
Controls whether a player can transfer a boat to another connected player.
-- @param canBequeath - boolean result of the built-in validation
-- @param source - integer server ID of the current owner
-- @param boat - Boat|nil boat instance
-- @param targetId - integer|nil target server ID
exports.jo_boats:registerFilter("canBequeathBoat", function(canBequeath, source, boat, targetId)
return canBequeath
end)Server canBuyBoat
Controls whether a player can buy a selected boat after its model and price have been resolved and before payment.
-- @param canBuy - boolean result of the built-in validation
-- @param source - integer server ID of the buyer
-- @param typeOrder - string "boat"
-- @param boatConfig - table selected model configuration
-- @param price - PriceClass selected and overwritten price
-- @param boatData - table purchase data received from the client
exports.jo_boats:registerFilter("canBuyBoat", function(canBuy, source, typeOrder, boatConfig, price, boatData)
return canBuy
end)Server canBuyCustomization
Controls whether a player can apply or remove a customization after category, ownership, and value validation.
-- @param canBuy - boolean result of the built-in validation
-- @param source - integer server ID of the owner
-- @param boat - Boat|nil boat instance
-- @param category - string customization category
-- @param value - number/hash selected value
exports.jo_boats:registerFilter("canBuyCustomization", function(canBuy, source, boat, category, value)
return canBuy
end)Client canCarryBoat
Adds custom restrictions before the local player carries a boat.
Safety checks cannot be bypassed
This filter runs only after the script has verified the boat, player state, distance, movement, occupants, wreck, and anchor conditions. Returning true cannot bypass those checks.
-- @param canCarry - boolean true by default
-- @param boat - Boat boat instance
exports.jo_boats:registerFilter("canCarryBoat", function(canCarry, boat)
return canCarry
end)Server canDeleteBoat
Controls whether a player can permanently delete a boat.
-- @param canDelete - boolean result of the built-in validation
-- @param source - integer server ID of the player
-- @param boat - Boat|nil boat instance
exports.jo_boats:registerFilter("canDeleteBoat", function(canDelete, source, boat)
return canDelete
end)Server canDockBoat
Controls whether an owner can dock a spawned, unanchored boat.
-- @param canDock - boolean result of the built-in validation
-- @param source - integer server ID of the player
-- @param boat - Boat|nil boat instance
-- @param shopId - string destination shop ID
exports.jo_boats:registerFilter("canDockBoat", function(canDock, source, boat, shopId)
return canDock
end)Server canOpenBoatStorage
Controls whether a player can open a spawned boat's configured inventory.
-- @param canOpen - boolean result of the built-in validation
-- @param source - integer server ID of the player
-- @param boat - Boat|nil boat instance
-- @param storageConfig - table|false model storage configuration
exports.jo_boats:registerFilter("canOpenBoatStorage", function(canOpen, source, boat, storageConfig)
return canOpen
end)Client canOpenShop
Controls whether the local player can open a shop.
-- @param canOpen - boolean true by default
-- @param shop - table shop configuration
exports.jo_boats:registerFilter("canOpenShop", function(canOpen, shop)
return canOpen
end)Server canRenameBoat
Controls whether a player can rename a boat after name, existence, and ownership validation.
-- @param canRename - boolean result of the built-in validation
-- @param source - integer server ID of the player
-- @param boat - Boat|nil boat instance
exports.jo_boats:registerFilter("canRenameBoat", function(canRename, source, boat)
return canRename
end)Server canRepairBoat
Controls whether an owner can repair a stored boat.
-- @param canRepair - boolean result of the built-in validation
-- @param source - integer server ID of the player
-- @param boat - Boat|nil boat instance
exports.jo_boats:registerFilter("canRepairBoat", function(canRepair, source, boat)
return canRepair
end)Server canSaveBoatHealth
Controls whether reported health values can be persisted when a world boat is removed.
-- @param canSave - boolean result of the built-in validation
-- @param source - integer server ID of the authoritative client
-- @param boat - Boat|nil boat instance
-- @param engineHealth - number|nil reported engine health
-- @param petrolTankHealth - number|nil reported petrol-tank health
-- @param bodyHealth - number|nil reported body health
exports.jo_boats:registerFilter("canSaveBoatHealth", function(canSave, source, boat, engineHealth, petrolTankHealth, bodyHealth)
return canSave
end)Client canSeeBlipForBoat
Controls whether a spawned boat blip is visible to the local player.
-- @param canSee - boolean true by default
-- @param boat - Boat spawned boat instance
-- @param blipConfig - table resolved blip configuration
exports.jo_boats:registerFilter("canSeeBlipForBoat", function(canSee, boat, blipConfig)
return canSee
end)Client canSeeBlipForShop
Controls whether a configured shop blip is visible to the local player.
-- @param canSee - boolean true by default
-- @param shop - table shop configuration
-- @param blipConfig - table resolved blip configuration
exports.jo_boats:registerFilter("canSeeBlipForShop", function(canSee, shop, blipConfig)
return canSee
end)Server canSpawnBoat
Controls whether a player can spawn a boat after ownership, world state, wreck, and spawn-limit validation.
-- @param canSpawn - boolean result of the built-in validation
-- @param source - integer server ID of the player
-- @param boat - Boat|nil boat instance
exports.jo_boats:registerFilter("canSpawnBoat", function(canSpawn, source, boat)
return canSpawn
end)Client canUseShopVendorCamera
Controls whether opening a shop moves the camera to a close-up of the seller. Only checked when Config.shopVendorCamera is enabled and the shop has a seller NPC.
-- @param canUse - boolean true by default
-- @param shop - table shop configuration
exports.jo_boats:registerFilter("canUseShopVendorCamera", function(canUse, shop)
return canUse
end)Server overwritePrice
Replaces the resolved PriceClass before the player is charged. The arguments after source depend on typeOrder.
| Operation | typeOrder | Arguments after source |
|---|---|---|
| Buy a boat | "boat" | boatConfig, priceIndex, boatData |
| Rename a boat | "renameBoat" | boatId, name, priceIndex |
| Buy a customization | "tints", "propsets", "liveries", "lanterns", or "extras" | boatId, value, priceIndex |
| Repair a boat | "repairBoat" | boat, priceIndex |
-- @param price - PriceClass resolved price
-- @param typeOrder - string operation type from the table above
-- @param source - integer server ID of the player
-- @param ... - operation-specific arguments from the table above
-- @return PriceClass price charged to the player
exports.jo_boats:registerFilter("overwritePrice", function(price, typeOrder, source, ...)
return price
end)Server renameBoat
Replaces the trimmed boat name after payment and before it is persisted.
-- @param name - string requested boat name
-- @param source - integer server ID of the owner
-- @param boatId - integer boat ID
-- @return string final name to save
exports.jo_boats:registerFilter("renameBoat", function(name, source, boatId)
return name
end)Server updateMaxRefSpawnedByPlayer
Changes the maximum number of boats this player can have spawned at once.
-- @param maxSpawned - integer value from Config.maxSpawnByPlayer
-- @param typeRef - string "boat"
-- @param source - integer server ID of the player
-- @return integer maximum spawned boats for this player
exports.jo_boats:registerFilter("updateMaxRefSpawnedByPlayer", function(maxSpawned, typeRef, source)
return maxSpawned
end)Snippets
Restrict boat purchases by job
This server-side filter only allows fishermen to purchase boats while preserving every built-in validation failure.
exports.jo_boats:registerFilter("canBuyBoat", function(canBuy, source, typeOrder, boatConfig, price, boatData)
if not canBuy then return false end
local job = jo.framework:getJob(source)
if job ~= "fisherman" then
jo.notif.rightError(source, "Only fishermen can buy boats.")
return false
end
return true
end)Apply a boat-purchase discount
This example applies a 20% discount to boat purchases and leaves every other operation unchanged.
exports.jo_boats:registerFilter("overwritePrice", function(price, typeOrder, source, ...)
if typeOrder == "boat" then
return price * 0.8
end
return price
end)Customize boat-storage permissions
This example preserves missing-storage validation, keeps normal owner access, and additionally lets fishermen open boat storage.
exports.jo_boats:registerFilter("canOpenBoatStorage", function(canOpen, source, boat, storageConfig)
if not boat or type(storageConfig) ~= "table" then
return false
end
if canOpen then
return true
end
return jo.framework:getJob(source) == "fisherman"
end)