Lua & resources
Lua modules and require
Local modules, published dependency libraries and `require`: the per-resource cache, the permissions a module runs under, what reload does to it, and how a module differs from an export.
On this page7 sections
Split resource code into modules with require. A module returns its public value, typically a function table. Libraries such as PolyZone execute inside the importing resource.
local PZ = assert(require('@polyzone'))
local zone = PZ.CircleZone:Create({x=100, y=200, z=20}, 15, {name='meeting_point'})
print(zone:isPointInside({x=105, y=200, z=20})) -- true
This loads PolyZone into your resource's client Lua VM. It does not download a package from the internet, call a remote service, or share your zones with another resource or player.
Availability
require is a client-side global. The dedicated-server sandbox does not
expose it. For server code, declare ordered server_script entry points; use
server exports to call another server resource.
Local modules already exist. The @dependency syntax requires the newer client
implementation introduced with PolyZone in client 2.31.13+op77.67.
Installing a Lua package alone cannot add this capability to an older client.
Load a file from your own resource
Example resource layout:
taxi/
open77.lua
client/main.lua
shared/fares.lua
Publish the module as a downloadable file, not a second entry point:
-- taxi/open77.lua
resource 'taxi'
version '1.0.0'
client_script 'client/main.lua'
files { 'shared/fares.lua' }
-- taxi/shared/fares.lua
local Fares = {}
function Fares.calculate(metres)
return math.floor(10 + metres * 0.02)
end
return Fares
-- taxi/client/main.lua
local Fares, reason = require('shared.fares')
assert(Fares, reason)
print(Fares.calculate(1500)) -- 40
The name is relative to the resource root, not to client/main.lua.
Use dots for directories and omit .lua.
| Import | Resolution, in order |
|---|---|
require('shared.fares') |
Your resource's shared/fares.lua, then shared/fares/init.lua |
require('@my_library') |
The dependency's published init.lua, then init/init.lua |
require('@my_library/helpers.math') |
The dependency's published helpers/math.lua, then helpers/math/init.lua |
Do not write require('shared/fares.lua') or an absolute disk path. Module
names accept letters/digits, underscores, hyphens and dot-separated segments;
traversal such as .. is rejected. The slash after @my_library separates the
resource name from the module name; nested module directories still use dots.
files makes a file available to clients. It does not execute it. Conversely,
client_script executes an entry point automatically. Listing the same module
as an entry point and importing it can execute its initialization twice.
Import a dependency such as PolyZone
The server must install and load both resources. The caller declares its dependency and the permissions its own code needs:
-- my_job/open77.lua
resource 'my_job'
version '1.0.0'
dependency 'polyzone >=1.0.0'
client_script 'client/main.lua'
permissions { 'world.query' }
-- my_job/client/main.lua
local PZ, reason = require('@polyzone')
assert(PZ, reason)
local garage = PZ.BoxZone:Create({x=100, y=200, z=20}, 12, 8, {
name='garage', heading=30, minZ=19, maxZ=24,
})
garage:onPlayerInOut(function(inside)
print(inside and 'Entered garage' or 'Left garage')
end, 250)
Here world.query is needed to read the player's position, not to use
require itself. A purely mathematical module needs no engine permission.
Add world.debug only if you enable PolyZone debug drawing.
The dependency must be declared and running, and its module must be published
under files. require neither installs nor starts a missing dependency.
Imports always execute with the caller's permissions, memory budget and
resource lifetime, not the provider's privileges.
Publish your own library
-- my_library/open77.lua
resource 'my_library'
version '1.0.0'
auto_start true
files { 'init.lua', 'helpers/math.lua' }
-- my_library/helpers/math.lua
return { double = function(value) return value * 2 end }
-- my_library/init.lua
return { math = assert(require('@my_library/helpers.math')) }
Use the provider-qualified form for sibling imports inside a reusable library.
require('helpers.math') would search the consumer's resource, even when
that call is written inside my_library/init.lua.
The consumer declares dependency 'my_library >=1.0.0' and uses
local Library = assert(require('@my_library')). It does not list
@my_library/init.lua as a manifest script. A provider may use its own
@my_library name without declaring a dependency on itself.
Everything in files is client-distributed content. Never publish credentials,
private configuration or server-only logic there.
A library needs no script of its own
The manifest above is complete. files alone is a valid resource, and so is
files plus a server_script for a library that also publishes server-side
exports():
-- bt_lib/open77.lua: server exports plus client modules, no client entry point
resource 'bt_lib'
version '0.1.0'
auto_start true
server_script 'server/main.lua'
files { 'init.lua', 'client/raycast.lua', 'client/format.lua' }
The server delivers every resource that has client content, and files is
client content. On the client such a passive library gets an idle VM, reaches
Running without executing anything, and a dependant's require('@bt_lib/...')
resolves against its declared files. A server_script never reaches a client;
it does not have to be paired with a client_script, empty or otherwise.
The only manifest that is refused is one with neither a script nor files:
no_client_scripts on the client, "Resource contains no scripts or files." on
the server. web_files alone do not count; nothing could open them.
Builds up to 2.31.13+op77.82 are stricter. Their client refuses any
delivered resource without a client_script or shared_script, logs
no_client_scripts, and that single refusal fails the whole activation:
server resource activation failed: discovery:... failed=1, and the client
stays on "Verifying resources". Fixed in the release that follows op77.82. Until
your players run it, add an empty client_script 'client/noop.lua' to the
library; it costs nothing and can be removed afterwards.
Cache, ownership and reload
The first successful import executes the module synchronously. Later imports of the same resolved file in the same Lua VM return its cached value:
local first = assert(require('@polyzone'))
local again = assert(require('@polyzone/init'))
print(first == again) -- true: both resolve to polyzone/init.lua
If a module returns nothing or nil, the cached result is true. Return a table
explicitly when exposing an API. A module may deliberately return false;
check value == nil if false is a valid result for your library.
Two consuming resources have separate module instances. Within one VM, mutating the returned table affects other imports of that module in that VM; it does not affect another resource's copy.
Keep module initialization synchronous: do not call Wait or await an export
at file scope during require. Return functions that can later run in
scheduler coroutines. Circular imports return nil, 'circular_module_dependency'.
Stopping/reloading the consumer destroys its VM, cached modules and owned
tasks/handlers. Native objects still follow their individual API cleanup rules;
PolyZone clears its own blips and debug drawings. Dropping one Lua variable is
not equivalent to destroying a zone: call zone:destroy() when finished.
Updating the provider does not rewrite tables already held by consumers.
Reload the consuming resources to adopt the new library code. A fresh
require('@provider') still checks that the dependency is declared and running;
a cached value does not bypass those checks.
Require or exports?
| Need | Use | Where the code runs |
|---|---|---|
| Split your client code into files | Local require |
Your resource's VM |
| Reuse constructors, helpers, zones or callback-based Lua objects | require('@library') |
A separate library copy in each caller's VM |
| Ask another resource to open its UI or perform a service operation | Open77.exports.call(...) |
The provider's VM, asynchronously |
| Send information between client and server | Network events | Across the transport, with server validation |
Exports copy supported values across the VM boundary; they cannot transfer a
live Lua function or a working zone object with methods. That is why PolyZone
uses require for its classes. Its server-side TriggerZoneEvent service is
an export instead. See resource exports and
server exports.
Imported client code does not become authoritative. For money, inventory or access checks, validate the action on the server even when a client zone says the player is inside.
Troubleshooting
local library, reason = require('@my_library')
if library == nil then
print('Library could not be loaded: '..tostring(reason))
return
end
| Result | What to check |
|---|---|
module_dependency_not_declared |
Add the provider's resource name to the caller's dependency declaration. |
module_dependency_not_running |
Install/load the provider and fix any startup error before importing it. |
module '...' not found in resource VFS |
Check spelling, resource-root paths, downloaded files and the provider's files entries. |
invalid_module_name |
Omit .lua, use dot-separated folders, and avoid paths/URLs. An old client can also reject the new @ syntax. |
circular_module_dependency |
Remove the import cycle; extract shared definitions into a third module. |
| Syntax/runtime error text | Fix the module identified in the message; its first execution did not complete. |
Attempt to call nil require |
This is server-side code, not the client module runtime. |
Ordinary module lookup/loading failures return nil, reason; invalid argument
types can raise a Lua argument error. No package.path, native DLL loader,
HTTP importer or unrestricted filesystem access is exposed.
See the client API reference for the call signature and PolyZone for the complete zone API.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
