Grids (tm.grids)
Context: shared (client and server) for the cell math and the spatial index. The tracker listeners are tm.grids.onEnter and onExit on the client and onEnter on the server.
Splits the GTA V map into square, numbered cells so proximity work can look at a few cells instead of every entity.
- Map extent: 8992 by 12992 units, origin offset 4480 by 4480 (
tm.grids.size,tm.grids.offset). - Cell sizes:
16,32,64,128. Any other size raises an error. - Cells are numbered row by row from the south west corner, starting at 1.
- With
keepSize = truethe number also carries its size (size * 10000000 + cell). Every function that takes a cell number accepts both forms and detects the size from a sized number, so thesizeargument is then ignored. - Positions outside the map land in the nearest edge cell.
Cell functions
| Function | Returns |
|---|---|
tm.grids.get(coords, size, keepSize?) | The cell number of a position. |
tm.grids.getCoords(grid, size, keepSize?) | The vector3 centre of a cell (z is 0.0). |
tm.grids.getWidth(size) | How many cells span the map width. |
tm.grids.getNearby(input, size, range?) | Cells within range cells (a square), centre included. input is a cell number or a position, range defaults to 1. |
tm.grids.getImmediate(coords, size, keepSize?) | The cell plus its 8 neighbors (3 by 3, clipped at the map edge). |
tm.grids.getOuter(coords, size) | Only the surrounding cells, without the centre. |
tm.grids.getInRadius(coords, radius, size) | Cells whose centre lies within radius world units. The cell holding coords is always included. |
local cell = tm.grids.get(GetEntityCoords(PlayerPedId()), 64)
for _, c in ipairs(tm.grids.getNearby(cell, 64, 1)) do
-- look at bucket[c]
endSpatial index
tm.grids.index(size) builds an index that buckets ids by cell, so a radius or area query visits only the cells it touches.
local index = tm.grids.index(64)
index:add('shop1', coords, { label = 'Shop' }) -- add, or move when the id exists
index:move('shop1', newCoords) -- false when the id is unknown
index:remove('shop1') -- false when it was not there
local ids = index:queryRadius(playerCoords, 50.0) -- exact flat distance
local ids = index:queryArea(minX, minY, maxX, maxY) -- rectangle, edges included
local ids = index:queryCell(playerCoords) -- ids in the one cell
local x, y, data = index:get('shop1')
index:has('shop1')
index:each(function(id, x, y, data) end) -- do not add ids from inside
index:clear()
index.count -- number of idsIds can be any non-nil value. Empty buckets are dropped as ids move or are removed, so a long-lived index does not grow.
Tracker: hear when a player changes cells
tm.grids.onEnter and tm.grids.onExit tell you when the local player (client) or any player (server) crosses into another cell. The tracker lives in tm_lib and does nothing until somebody listens: no thread, no position reads, no network traffic.
tm.grids.sizes is { 16, 32, 64, 128 }. A tracker is addressed by its index (position in that list) or by the cell size itself, so 3 and 64 mean the same thing.
-- client
local listener = tm.grids.onEnter(64, function(grid, nearby)
-- grid: the new cell, with its size in the number (like tm.grids.get(coords, 64, true))
-- nearby: the 3 by 3 block of cells around it
end)
tm.grids.onExit(64, function(grid, nearby)
-- the cell that was left and the block that surrounded it, fired just before the matching enter
end)
listener.remove() -- stop listening; safe to call twice
local grid, nearby = tm.grids.current(64) -- what the tracker last announced, nil while nobody listens-- server
local listener = tm.grids.onEnter(32, function(source, grid)
-- grid carries its size, like tm.grids.get(coords, 32, true)
end)
local cell = tm.grids.ofPlayer(source, 32, true) -- the cell the server sees the player in, nil without a ped| Rule | Detail |
|---|---|
| First call | A listener hears the current cell on the next frame (client). A listener added while the size is already tracked is called once right away with the current cell. |
| Events | Plain local events: tm_lib:grids:enter<N>(grid, nearby) and tm_lib:grids:exit<N>(grid, nearby) on the client, tm_lib:grids:enter<N>(source, grid) on the server, N being the index. tm.grids.enterEvent(size) and exitEvent(size) return the names. Register interest with onEnter/onExit first, the tracker only runs for sizes somebody registered. |
| Cleanup | Everything a resource registered is released when it stops, and registered again if tm_lib restarts. |
| Server exits | Not reported to the server. Keep the last cell per player if you need it. |
| Trust | The server checks every client report: it must be a real cell of the right size, arrive at a sane rate and match the position the server knows for the ped (two and a half cell widths of slack, checked again for about a second before it is dropped, because the server position lags a teleport). A handler can trust grid like GetEntityCoords(GetPlayerPed(source)). |
How cheap is it
- The thread exists only while a size is wanted. It ends by itself when the last listener goes.
- It looks at the player between every 100 ms and every 500 ms: the faster the player moves, the sooner, based on the smallest wanted cell size. Standing still costs two position reads a second.
- It does no cell work while the player has moved less than a meter since the last look.
- A size nobody wants is never computed. The nearby block is built only when the cell actually changes.
The /tmgrids command
Staff only (checked on the server). Toggles a display of the grid cells around you, one layer per cell size: yellow is your cell, green is the 3 by 3 block. The draw loop exists only while the display is on.