Map, markers & zones
PolyZone
Polygons, boxes, circles and spheres, entity zones and groups: containment queries, entry and exit callbacks, and the in-game zone editor with native debug drawing.
On this page8 sections
Use the PolyZone API in an Open77 client resource with ordinary {x, y, z} coordinates. Queries and watchers run inside the importing resource. Names follow PolyZone 2.6.2; engine integration and the editor are Open77-specific.
Source: polyzone.
Install and import
New to require? Read Lua modules and require for local
modules, published dependencies, caching and the difference from exports.
Add polyzone and your resource to the server's resources.load. Declare:
dependency 'polyzone >=1.0.0'
permissions { 'world.query', 'world.debug', 'ui.vanilla.map', 'network.events', 'local.events' }
Only declare permissions you use: mathematical containment needs none;
player/entity reads need world.query; debug drawing needs world.debug;
debug map blips need ui.vanilla.map; network/local event operations need the
corresponding event permission.
local PZ = assert(require('@polyzone'))
local PolyZone, BoxZone = PZ.PolyZone, PZ.BoxZone
local CircleZone, EntityZone, ComboZone = PZ.CircleZone, PZ.EntityZone, PZ.ComboZone
-- Optional alternative: PZ.installGlobals()
This requires the client update adding dependency modules and native geometry;
installing the Lua folder alone does not upgrade an older client. The import
returns nil, reason on a missing/stopped/undeclared dependency.
Shapes
local plaza = PolyZone:Create({
{x=100,y=100}, {x=115,y=100}, {x=115,y=110},
{x=108,y=110}, {x=108,y=120}, {x=100,y=120},
}, {name='plaza', minZ=20, maxZ=28, data={job='taxi'}})
local garage = BoxZone:Create({x=150,y=100,z=20}, 12, 6, {
name='garage', heading=45, minZ=19, maxZ=25,
scale={1,1,1}, offset={0,0,0},
})
local area = CircleZone:Create({x=120,y=120,z=20}, 15, {name='area'})
local sphere = CircleZone:Create({x=120,y=120,z=20}, 15, {name='sphere',useZ=true})
- Polygon: concave XY winding-number containment; optional inclusive Z limits. Concave shapes are supported; use simple, non-self-intersecting polygons. Edge classification follows winding-number rules; it is not universally inclusive. Z is independent of the Z values of polygon vertices.
- Box:
lengthis local Y,widthis local X. Heading is degrees rotating around +Z. Box faces and Z limits are inclusive.scale/offsetaccept three symmetric values{length,width,height}or six asymmetric values{forward,back,left,right,up,down}. Offsets are metres, scales multipliers. - Circle: unbounded in Z by default.
useZ=truemakes a sphere. The radius comparison is strict: a point exactly on the radius is outside.
Common options: name, data, debugPoly, debugBlip, debugColors
(walls, outline, grid, RGB arrays), debugColor (circle RGB), and the
Open77 extension debugDistance (default 250 metres).
Methods
| Class | Methods |
|---|---|
| All zones | :new(...), :Create(...), :isPointInside(point), :onPointInOut(getPoint,callback,intervalMs?), :onPlayerInOut(callback,intervalMs?), :setPaused(bool), :isPaused(), :destroy(), :draw(force?), :addEvent(name), :removeEvent(name), :addDebugBlip() |
| Polygon / box | :getBoundingBoxMin(), :getBoundingBoxMax(), :getBoundingBoxSize(), :getBoundingBoxCenter(), :TransformPoint(point) |
| Box | :getHeading(), :setHeading(degrees), :setCenter(point), :getLength(), :setLength(metres), :getWidth(), :setWidth(metres) |
| Circle / sphere | :getCenter(), :setCenter(point), :getRadius(), :setRadius(metres) |
| Entity | Box containment plus :getCenter(), :onEntityDamaged(callback); the entity's transform overrides manual placement setters |
| Combo | :AddZone(zone), :RemoveZone(nameOrPredicate), :getZones(point), :isPointInside(point,name?), :isPointInsideExhaustive(point,outArray?), :onPointInOutExhaustive(getPoint,callback,intervalMs?), :onPlayerInOutExhaustive(callback,intervalMs?), :addEvent(eventName,zoneName?), :printInfo() |
getBoundingBox* refers to the box's unrotated local-axis bounds placed at
its centre, as upstream does; it is not a world-axis AABB of the rotated box.
Use TransformPoint on corners when you need world corners.
Utilities: PolyZone.getPlayerPosition(), PolyZone.getPlayerHeadPosition(),
PolyZone.rotate(origin,point,degrees), PolyZone.drawPoly(zone,force?),
BoxZone.calculateMinAndMaxZ(minZ,maxZ,scaleZ,offsetZ) and
PolyZone.ensureMetatable(value) (rehydrates methods without exposing metatable
mutation; serialized callbacks, references and debug registrations are not restored).
Prefer constructors for saved configuration. Open77 extensions: PZ.list(),
PZ.destroyAll(), PZ.serialize(zone) and watcher cancellation handles.
Entry / exit and groups
local all = ComboZone:Create({plaza, garage, area}, {name='job_areas'})
local cancel = all:onPlayerInOutExhaustive(function(inside, position, current, entered, left)
for _, zone in ipairs(entered or {}) do print('enter '..zone.name) end
for _, zone in ipairs(left or {}) do print('exit '..zone.name) end
end, 250)
Polling defaults to 500 ms; 0 checks each frame. These are sampled positions,
not swept collision tests: a player can cross a thin zone between polls.
Base shapes emit only state transitions from an initial outside state; Combo
also emits its first outside sample. The normal Combo callback is
callback(inside, point, zone) and does not report overlapping member changes
while the combined flag stays true. The exhaustive callback above does.
entered/left are nil when there is no change in that direction.
setPaused(true) pauses callbacks, not explicit containment queries. cancel()
removes only that watcher. Destroying a Combo destroys its remaining children;
RemoveZone returns the removed child without destroying it. Avoid sharing
children between independently owned groups if destroying one must not affect
the other. Cyclic groups are rejected. Member priority follows insertion order.
Missing player incarnation or a streamed-out entity produces an outside state, not a fake position at world origin. It can emit one exit and later re-enter. Head position is nil when the native Head slot is unavailable.
Moving entities
-- Typed network references survive a change of local presentation handle.
local trunk = EntityZone:Create({kind='vehicle',id=vehicleId}, {
name='vehicle_area', useZ=true, offset={1,1,0.5},
})
local npcArea = EntityZone:Create({kind='npc',id=npcId}, {useZ=true})
local playerArea = EntityZone:Create({kind='player',id=playerId}, {useZ=true})
local localPlayerArea = EntityZone:Create(0, {useZ=true})
A numeric value is a generation-checked local entity handle, not a network
vehicle ID or raw REDengine pointer. 0 denotes the local player. Prefer typed
IDs for replicated entities. Headings and Z envelopes follow the entity;
pitch/roll are included when computing the eight-corner vertical envelope.
XY containment remains a yaw-oriented box, matching the EntityZone convention.
Dimensions come from currently loaded static/skinned component mesh bounds,
including camera-hidden body parts so switching to FPP does not remove them.
They are reference mesh bounds, not an animation-deformed collision hull or GTA
model dimensions. If bounds are unavailable, the
zone is outside and exposes zone.error. You can supply explicit local-space
dimensions={min={x,y,z},max={x,y,z}} to define a stable interaction volume.
No new asset streaming is requested by the geometry query.
onEntityDamaged(function(died, attackerId, weapon, isMelee, details) ... end)
observes canonical health decreases at 50 ms intervals. details includes
health and revision. This is not a per-hit GTA event: damage followed by
healing between samples can be missed, and several hits may collapse into one
observation. attackerId is a network player ID when available; weapon is nil
when the protocol provides no weapon. Do not treat these as GTA handles/hashes.
Use server combat events for authoritative damage logic.
Grid and debug performance
Polygons support useGrid (default true), lazyGrid (default true), and
gridDivisions (default 30, range 1β256). Full interior cells skip the winding
test; edge cells still use exact containment. Eager construction yields to the
resource scheduler. debugGrid=true builds the grid eagerly.
Combos use a sparse broadphase. Moving entities and nested combos are checked dynamically; resizing/repositioning a static member invalidates the index. There is no permanent polling coroutine when a VM has no watchers or debug zones.
Debug wireframes/walls use Open77's native compositor, not a continuously updated
CEF page. They are developer overlays, visible through scenery, not depth-tested
meshes. Geometry is bounded by quotas and expires if not refreshed. Large debug
grids display a bounded subset of cells; containment still uses the full grid.
An oversized debug shape can exceed the drawing quota without affecting its
containment queries. Debug geometry is composited below WebUI menus.
Keep debugPoly/debugGrid disabled in normal player resources.
Zone-filtered events
-- Client, in a resource importing PolyZone:
plaza:addEvent('taxi:announcement')
AddEventHandler('taxi:announcement', function(text) print(text) end)
-- Server, in a coroutine / command / event handler:
local pending, reason = Open77.exports.call('polyzone', 'TriggerZoneEvent', 'taxi:announcement', 'Taxi available')
if pending then
local ok, error = pending:await()
if not ok then print(error) end
else print(reason) end
The provider broadcasts __PolyZone__:<event> and the consumer delivers the
local event only if the player is inside. There is deliberately no arbitrary
client-to-server broadcast relay. Client zone checks are conveniences, not
security boundaries: the server must validate proximity, buckets and job rules
before awarding money/items or authorizing actions.
Editor
Grant command.pzcreate, command.pzadd, command.pzundo, command.pzfinish,
command.pzlast, command.pzcancel, command.pzcomboinfo, command.pzedit
to trusted developers using the server ACL.
/pzcreate poly|box|circle [name] [length-or-radius] [width]: begin at your feet./pzadd,/pzundo: add/remove a polygon point (maximum 128 editor vertices)./pzedit: show/hide the WebUI editor; F9 releases/reacquires focus.- F10: add a vertex while walking. Fields edit size, heading, height or sphere mode.
/pzfinish: produce Lua, keep a local KVP backup and request a server save./pzlast [name]: repeat the last completed box/circle at your current position./pzcancel: discard the draft./pzcomboinfo: print active Combo counts.
Exports append to polyzone/data/polyzone_created_zones.txt; the server checks
command.pzfinish again and limits size/frequency. Generated code is never
executed automatically. Use Copy to paste it into your own resource.
The editor is a local authoring tool; the server ACL protects persistent saves.
New engine API and lifecycle
Open77.world.entityGeometry(handleOrTypedReference) (world.query) returns
{attached,entity,position,forward,orientation,bounds?,head?,damage?} or nil,reason.
bounds contains min, max, source='loaded_mesh_bounds'. orientation is
{x,y,z,w}. Missing bounds/head/health are omitted, never fabricated.
local ok, reason = Open77.debugDraw.set('my-area', {
lines={{a={x=0,y=0,z=20},b={x=5,y=0,z=20},color='#19d5e0ff'}},
triangles={}, ttl=0.5, maxDistance=250, thickness=1.5,
})
Open77.debugDraw.clear('my-area') -- clear() clears only this resource's geometry
world.debug is needed to set geometry. Coordinates must be finite and bounded
to Β±1,000,000. Colours use #RRGGBB or #RRGGBBAA. Limits: 512 lines and 256
triangles per batch, 32 tags per resource, 128 total batches, 8,192 total input
vertices. TTL is 0.05β10 seconds, maximum distance 1β5,000 metres, thickness
0.5β8 pixels. Replacing a tag is atomic; invalid input returns nil,reason.
require('@provider/module') runs only an explicitly published files entry of
a declared, running dependency. It executes in the caller's VM, budget and
permissions. Imports are cached by resolved module path; circular imports are
rejected. Each consumer therefore owns independent zones and callbacks. No
Lua function or mutable zone object crosses a VM boundary.
Stopping/reloading a consumer cancels its tasks/handlers and releases its blips and debug drawings. Startup failure, task failure and validation cleanup also release native drawings; TTL is a backstop. Reload consumers to pick up changed library code: existing Lua references do not silently change implementation. No wire-protocol change or dedicated-server C# change is required for this port.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
