Skip to content

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 = true the 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 the size argument is then ignored.
  • Positions outside the map land in the nearest edge cell.

Cell functions ​

FunctionReturns
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.
lua
local cell = tm.grids.get(GetEntityCoords(PlayerPedId()), 64)
for _, c in ipairs(tm.grids.getNearby(cell, 64, 1)) do
    -- look at bucket[c]
end

Spatial 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.

lua
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 ids

Ids 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.

lua
-- 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
lua
-- 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
RuleDetail
First callA 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.
EventsPlain 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.
CleanupEverything a resource registered is released when it stops, and registered again if tm_lib restarts.
Server exitsNot reported to the server. Keep the last cell per player if you need it.
TrustThe 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.