Resource Nodes

Version 1.0.0. The listing, or pick it up in the shop and run it.

Resource Nodes

Resource Nodes gives every rock, tree, fishing spot and herb patch a stock that runs down when people harvest it and grows back over real time. Most gathering scripts hand out stone, wood and fish forever, so anyone can stand at a rock for an hour and flood the market, and gathered items end up worth nothing. Here supply is limited, so what players gather has a value, and staff can see where it's running out before it does.

Supply screen

What you get

  • Node types you define: what a harvest gives (several items with weights and amounts), the tool needed, harvest time, how much a node holds and how fast it grows back, plus an optional prop and animation.
  • Fields of nodes: list the spots by hand, or give a circle and a number and the nodes get placed for you, the same way every restart.
  • Finite stock that persists. Each node keeps its stock in the database, so a restart never refills the map. Regrowth is worked out from timestamps when it's needed, so nothing ticks per node.
  • Optional limits: a job list per field, and a daily cap per character per node type that survives reconnects and restarts.
  • Players see the state: a marker that turns grey when a node is empty, a "Rock 3/5" label up close, and props that can change or disappear when a node runs dry.
  • A supply screen (/nodes) with every field's stock, harvests in the last hour and day, regrowth, a "days until empty at this rate" estimate, stock per node, what a harvest gives and the top gatherers. You can refill or freeze a field from there too.
  • Server-side everything: distance (from the server's own copy of the player's position), tool, stock, job, daily cap and inventory space are all checked on the server, the harvest timer runs on the server, and each player harvests one thing at a time.
  • Exports for shops and other scripts: current supply per type, and a price multiplier that rises as supply falls.
  • Framework support: ESX Legacy, Qbox and QBCore, with ox_inventory, qb-inventory, the ESX inventory, or an inventory that answers ox_inventory's exports.

Requirements

  • ESX Legacy, Qbox or QBCore
  • oxmysql
  • OneSync (the server checks where players stand)
  • ox_inventory, qb-inventory or the ESX inventory. An inventory that keeps its own item list and answers ox_inventory's Items export (for example ak47_qb_inventory) is checked against its own list.
  • ox_lib is optional. When it runs, prompts and notifications use it.

Install

  1. Unzip so that resources/resource_nodes/fxmanifest.lua exists (not resources/resource_nodes-v1.0.0/resource_nodes/..., which Windows "Extract All" creates), then add ensure resource_nodes. Start order doesn't matter for the framework, it waits up to 30 seconds for it and prints what it found.
  2. Give staff the supply screen in server.cfg:
    add_ace group.admin resourcenodes.admin allow
    
  3. Add the items to your inventory (next section), or rename the items in config.lua to ones you already have. On start the server prints any item your inventory doesn't know.
  4. Start the server. The tables create themselves (sql/install.sql is there if your database user can't create tables).
  5. Go to Davis Quarry with a pickaxe and press E at a rock. Staff open the supply screen with /nodes.

Items

Stock frameworks don't ship stone, logs, fish or the tools. The shipped config uses these names: pickaxe, hatchet, fishingrod, stone, iron, copper, diamond, wood_log, tree_bark, fish, fish_bass, fish_rare, herb_sage, herb_aloe. QBCore already has iron, copper and diamond.

ox_inventory, in ox_inventory/data/items.lua:

['pickaxe'] = { label = 'Pickaxe', weight = 1500, stack = false },
['stone'] = { label = 'Stone', weight = 500 },
['wood_log'] = { label = 'Wood log', weight = 1000 },

qb-inventory, in qb-core/shared/items.lua:

pickaxe = { name = 'pickaxe', label = 'Pickaxe', weight = 1500, type = 'item', image = 'pickaxe.png', unique = true, useable = false, shouldClose = false, description = 'For breaking rock' },
stone = { name = 'stone', label = 'Stone', weight = 500, type = 'item', image = 'stone.png', unique = false, useable = false, shouldClose = false, description = 'A chunk of rock' },

ESX inventory, in your database (INSERT IGNORE skips items you already have):

INSERT IGNORE INTO items (name, label, weight) VALUES ('pickaxe', 'Pickaxe', 2), ('stone', 'Stone', 1), ('wood_log', 'Wood log', 1);

Setting it up

Everything lives in config.lua, with a comment next to each setting.

Node types

stone = {
    label = 'Rock', verb = 'mine', tool = 'pickaxe', time = 6,
    stockMax = 6, regrowPerHour = 2, dailyCap = 150,
    yields = {
        { item = 'stone', weight = 70, min = 1, max = 3 },
        { item = 'iron', weight = 22, min = 1, max = 2 },
        { item = 'copper', weight = 7, min = 1, max = 2 },
        { item = 'diamond', weight = 1 },
    },
    prop = 'prop_rock_4_c',
    scenario = 'WORLD_HUMAN_CONST_DRILL',
},
  • One harvest takes one unit of stock. stockMax is how many units a full node holds, and regrowPerHour is how many it gets back each real hour.
  • A harvest picks one yields line by weight, then an amount from min to max. Weights are relative, so 70, 22, 7 and 1 work out to 70%, 22%, 7% and 1%.
  • tool is checked when the harvest starts and again when it ends, and it's never used up. Leave it out for no tool.
  • dailyCap is harvests per character per day for this type. The day starts at Config.DailyResetHour, server time.
  • prop is spawned at each node on the player's side only. Add depletedProp to swap it when the node is empty (IE a stump for a tree), or leave it out to hide the prop.

Fields

{
    id = 'davis_quarry', label = 'Davis Quarry', type = 'stone',
    zone = { center = vector3(2950.0, 2790.0, 42.0), radius = 30.0, count = 10, seed = 7, spacing = 4.0 },
    job = { 'miner' },                                  -- optional
    blip = { sprite = 319, color = 5, scale = 0.7 },   -- optional
},
{
    id = 'alamo_pier', label = 'Alamo Sea pier', type = 'fish',
    coords = { vector3(1299.4, 4218.9, 33.9), vector3(1301.7, 4231.6, 33.9) },
},
  • A zone places count nodes inside the circle, at least spacing apart. The same seed always gives the same spots, and changing the radius, count or seed moves the nodes.
  • A zone only knows its center's height, so the server allows Config.HeightSlack up or down. On steep ground, list the coords by hand instead.
  • Stock is saved under the field id and the node's number. Don't rename a live field, or it starts full again.

Balancing supply

The rough math: a field of 10 nodes at 2 units an hour grows back 20 units an hour, about 480 a day. If your players harvest more than that, the field runs down, and the supply screen tells you in how many days. If it empties every evening, raise regrowPerHour or add nodes. If nobody ever finds an empty rock, lower them.

The supply screen

/nodes opens it for anyone with the ace above. Per field you see:

Column What it means
Status Healthy, Draining (empty within 3 days), Low (under a quarter, or empty within a day), Empty or Frozen
Stock Units ready now against the most the field can hold
Last hour, 24 hours Harvests from the log
Regrowth Units the whole field gets back per hour
Empty in Stock left divided by harvests minus regrowth, at the last 24 hours' pace. "never" means it grows back as fast as it's picked

Select a field for the stock of each node, how long until it's full with no harvests, what a harvest gives, and the top gatherers of the last Config.TopWindowDays days.

  • Refill puts every node in the field back to full.
  • Freeze closes the field. Nobody can harvest and nothing grows back until you unfreeze it, and harvests in progress stop and give their unit back. Freezing is saved, so it survives a restart.

Both buttons are checked against the ace on the server on every click. From the console or in game you can also type nodes refill <field id>, nodes freeze <field id> or nodes unfreeze <field id>. Typing nodes alone in the console prints every field's stock.

How a harvest works

  1. The player presses E at a node. The server checks the node exists, the field is open, the job, that the player stands next to it (using its own copy of their position, never what the client says) and that they carry the tool.
  2. It loads their daily count, rolls what the harvest will give and checks it fits in their inventory. Then it takes one unit from the node straight away, so two players can't both get the last one.
  3. The server runs the timer. Walking away, pressing X, switching character, disconnecting or a freeze stops it, and the unit goes back to the node.
  4. When the timer ends it checks the tool and the inventory space again and gives the item. If the inventory refuses it, the unit goes back to the node.
  5. The node's new stock, the player's daily count and a log line are saved.

Requests faster than a person can press a key get dropped, and each player can only run one harvest at a time.

Exports

local stock, max = exports.resource_nodes:GetSupply('stone')                -- units ready, units when full (nil for an unknown type)
local stock, max, frozen = exports.resource_nodes:GetFieldSupply('davis_quarry')
local mult = exports.resource_nodes:GetPriceMultiplier('stone')             -- see below
exports.resource_nodes:RefillField('davis_quarry')                          -- true when done
exports.resource_nodes:SetFieldFrozen('davis_quarry', true)

Prices that follow supply

GetPriceMultiplier slides from Config.Market.empty when every node of a type is empty to Config.Market.full when they're all full (1.6 and 0.9 by default). Nothing changes prices on its own, so use it wherever your shop or buyer sets a price:

local price = math.floor(12 * exports.resource_nodes:GetPriceMultiplier('stone'))

With Economy Pulse, multiply its live price, so inflation and scarcity both count:

local base = exports.economy_pulse:GetPrice('stone') or 12
local price = math.floor(base * exports.resource_nodes:GetPriceMultiplier('stone'))

Resource Nodes doesn't need Economy Pulse, and neither one needs the other to start.

Commands

Command Who What
/nodes ace resourcenodes.admin Supply screen
/nodes refill|freeze|unfreeze <field id> ace resourcenodes.admin or console Refill, freeze or unfreeze a field
nodes console Every field's stock as text

Troubleshooting

  • Nothing happens and there's no [resource_nodes] line in the console: the folder is most likely nested. Check that resources/resource_nodes/fxmanifest.lua exists, and not resources/resource_nodes/resource_nodes/fxmanifest.lua.
  • "No framework finished starting within 30s": your framework isn't running or it has a different resource name. The line lists what it found. Set Config.Framework if you run a renamed core.
  • "doesn't know these items": add those items to your inventory (see Items) or rename them in config.lua. Case doesn't matter.
  • Every harvest says "Get closer first": OneSync is off (the console says so on start), or the node sits on a slope higher or lower than Config.HeightSlack from the zone's center. List that field's coords by hand.
  • "Could not read saved stock, so nothing was started": the database answered with an error when loading stock. The resource stops so it doesn't refill every field. Fix the database connection and restart it.
  • "Database setup failed": oxmysql isn't connected or your database user can't create tables. Run sql/install.sql by hand.
  • A prop doesn't show, only the marker: the model name isn't in the game files. The F8 console names it. Pick another model or remove prop.
  • Stock looks the same for a minute after it should have grown: players get regrown stock every Config.SyncSeconds (30 by default). Harvests and staff changes show straight away.

Escrow

The core is encrypted with Cfx asset escrow. These stay open so you can adapt them: config.lua, server/bridge.lua (all the framework and inventory code lives there) and sql/install.sql.

Support

Open a ticket in the Discord with your framework, inventory, oxmysql version and the server console output from startup.