WorldBridge on OpenSimulator var regions
WorldBridge now works on OpenSimulator var regions (the modern "mega regions": one Scene that is 512 x 512 = 2x2,
768 x 768 = 3x3, and so on), not only the classic 256 x 256. Every region-coordinate default and clamp reads the
bound Scene's real size (m_scene.RegionInfo.RegionSizeX / RegionSizeY). Heightmap indexing uses the heightmap's
own Width / Height. A 256 region behaves exactly as before. The one visible difference there is two new
response fields: state gains sizeX/sizeY, and heightmap gains samplesX/samplesY.
Facts about var regions in this OpenSim (from OpenSim/Framework/RegionInfo.cs, DoRegionSizeSanityChecks):
each side is a multiple of 256, up to Constants.MaximumRegionSize = 4096, and the region is forced square
because of viewer limits. So RegionSizeX == RegionSizeY in practice. The code still reads both, in case a future
OpenSim lifts that rule.
Helpers added
WorldBridgeModule.cs:1001-1004: RegionSizeX, RegionSizeY (float metres, falling back to
Constants.RegionSize if no scene is bound yet), RegionMidX, RegionMidY (half of each; 128 on a 256 region).
Roam.cs shares them through the partial class.
Every site changed
Line numbers are on branch ship/var-regions, with the old line on 2c9f690 in brackets.
| # | Where | Before | After |
|---|---|---|---|
| 1 | WorldBridgeModule.cs:715-716 state (old 713-714) |
region, regionUUID, avatars, objects | adds sizeX, sizeY (metres, from RegionInfo), so a client can learn the extent |
| 2 | WorldBridgeModule.cs:1045-1070 heightmap (old 1034-1050) |
fixed n = 64, stepX = w/64, stepY = h/64; body ignored |
optional {"grid":N} (default 64, clamped [8,512]); one square step ceil(max(w,h)/N); adds samplesX, samplesY. Contract below |
| 3 | WorldBridgeModule.cs:1560 wander (old 1540) |
cx, cy default 128 |
default RegionMidX/Y. The clamp stays [0,10000], which already covers any var region |
| 4 | WorldBridgeModule.cs:1808-1813 create_account home (old 1788) |
SetHome(..., new Vector3(128,128,26)) |
the middle of the home region h, by h.RegionSizeX/Y (a GridRegion, which may be the grid's default region and not this one). Falls back to 128 if the size is 0 |
| 5 | WorldBridgeModule.cs:2302 chat_send (old 2277) |
speaks from (128,128,30) when no avatar is named |
(RegionMidX, RegionMidY, 30) |
| 6 | WorldBridgeModule.cs:2329 teleport (old 2304) |
x,y default 128, clamp [0,256] |
default RegionMid, clamp [0,RegionSize] |
| 7 | WorldBridgeModule.cs:2406 move_object (old 2381) |
clamp [0,256] |
clamp [0,RegionSize] |
| 8 | WorldBridgeModule.cs:2458 npc_create (old 2433) |
default 128, clamp [0,256] |
default RegionMid, clamp [0,RegionSize] |
| 9 | WorldBridgeModule.cs:2473 npc_move (old 2448) |
default 128, clamp [0,256] |
default RegionMid, clamp [0,RegionSize] |
| 10 | WorldBridgeModule.cs:3039 raycast (old 3014) |
origin default (128,128,30) |
(RegionMidX, RegionMidY, 30). The clamp stays +/-1e4 |
| 11 | Roam.cs:210-211 roam range centre (old 210) |
cx, cy default 128, clamp [0,256] |
default RegionMid, clamp [0,RegionSize] |
| 12 | Roam.cs:212-214 roam radius (old 211) |
clamp [5,180] |
clamp [5, 180 * max(SizeX,SizeY) / 256]: 180 on 256, 360 on 512, 540 on 768. 180 m is a 256 region's half-diagonal (the whole region from its middle), so the same reach scales with the region |
| 13 | Roam.cs:704-724 BuildGrid (old 703-709) |
grid window = range +/- 2 m, clipped to the heightmap | same, plus a hard cap MaxWalkGridSide = 1024 cells a side, centred on the range. It never binds on a region of 1024 m or less, because the heightmap bound is tighter |
Later (2026-09-27, multi-region, ship/p2-wb): the position clamps in rows 6-9 and rez_item stop at
RegionMaxX/Y = size - 0.01 m instead of the size itself. OpenSim's region is [0, size): a position clamped to
256.0 sat on the border, and beside a neighbouring region an object moved there crossed into it (seen on a two-region
test server). The roam range centre (row 11) still clamps to the size; it is a centre, not a position.
Build: dotnet build worldbridge/WorldBridge.csproj -c Release -p:OpenSimBin=C:/CLD/paracosm/opensim-src/bin/
gives 0 warnings, 0 errors. The -p:OpenSimBin= override is needed only in a worktree, which has no
opensim-src/. It points the HintPaths at the live bin, read-only. The csproj is unchanged.
The heightmap op contract
Request body is optional: {} or { "grid": N }.
Nis the number of samples along the region's longer side. It defaults to 64 and is clamped to [8, 512]. Unparseable JSON is treated as{}, because the op ignored its body before and old callers must still work.- There is one square step for both axes:
step = ceil(max(width, height) / N)metres, andstepX == stepY == step. - Samples are point samples:
grid[row][col] = height(x = col*step, y = row*step)in metres, rounded to 0.01. The last column sits atx = width - step, notwidth - 1. That is unchanged from before. - The response is
{ ok, width, height, stepX, stepY, samplesX, samplesY, grid }.width/heightare the heightmap's size in metres, which equals the region's size.samplesX/samplesYaregrid[0].lengthandgrid.length.
| region | {} |
{"grid":256} |
{"grid":512} |
|---|---|---|---|
| 256 | 64 x 64 @ 4 m (identical to before) | 256 x 256 @ 1 m | 256 x 256 @ 1 m |
| 512 | 64 x 64 @ 8 m | 256 x 256 @ 2 m | 512 x 512 @ 1 m |
| 768 | 64 x 64 @ 12 m | 256 x 256 @ 3 m | 384 x 384 @ 2 m |
Why this option: the default keeps the output shape stable (64 x 64) on every size, so existing renderers still
get a grid they can draw. grid lets a client ask for enough resolution on a big region, where 12 m point samples
alias small features like a jetty or a narrow sandbar. The 512 cap bounds a response to 262k numbers (about 2 MB of
JSON). One square step was chosen over independent per-axis steps so that cells stay square even if a non-square
region ever becomes legal. On today's forced-square regions the two choices are identical.
Walk-grid cost on a 768 region (roam)
The walk grid is N x N one-metre cells. N is the range window, capped by the heightmap to at most side - 3: 253 on 256, 509 on 512, 765 on 768. The things that fill it are:
- ordinary ranges: radius <= 180 by default use. N = 2R+4, so it does not depend on region size at all (R = 40 gives N = 84 on any region);
- big ranges: now allowed up to 540 m on a 768 region;
- water trips: the
Tripgrid spans the range plus the pool, with radiusdist/2 + R + 8, and that radius was never clamped. On a 256 region the heightmap held this grid to 253^2; on a 768 region it can reach 765^2.
Measured on the live 256 region (OpenSim.log, 2026-09-27): a 157 x 157 grid builds in 0.3-0.7 s. That is about 30 us per cell for one downward physics ray and five bilinear ground reads. The worst case on a 768 region, N = 765 (585k cells), comes to:
- held: 9 B/cell (Blocked + ClearDist + Slope), about 5.3 MB per grid;
- build: about 585k rays, roughly 15-20 s on its own background thread (
WorldBridge-walkgrid). Each ubODE ray is queued to the physics step, so a build competes with other raycasts, but it never blocks the 1 Hz tick; - per journey planned on it:
dist/len/prevare 12 B/cell, about 7 MB allocated, twice if the scramble (relax) pass runs. A full N^2 scan picks the goal. The Dijkstra flood itself stays bounded byLegMax * 1.8. Trip plans happen once per thirst cycle (900-1800 s by default), so this is occasional large-object-heap churn, not a steady load.
These costs are sane for 768. Past 1024 on a side, which only happens on a var region bigger than 4x4 (OpenSim
allows up to 4096), a 4093^2 grid would be about 150 MB held and 200 MB per plan. MaxWalkGridSide = 1024
caps N, so the worst case anywhere is 1M cells: about 9.4 MB held, 12.6 MB per plan, and about 30 s to build.
Cells outside a cropped grid are simply not walkable, because Usable's g.In() check rejects them. A water trip
longer than about 1 km on such a region will fail to plan and retry a minute later. It will not hang or throw.
Region-size-dependent things left alone, and why
set_sit_target(:2272), x/y/z clamped to [-256, 256]: this is a sit offset in the prim's local frame, not a region coordinate. 256 m is far beyond any usable sit offset on any region size.scale_object(:2439), clamped to [0.01, 256]: this is the prim size limit, which matches OpenSim'sNonPhysicalPrimMaxdefault of 256. It is not the region footprint.- z clamps of 4096 (
teleport,move_object,npc_*): this is the region height (Constants.RegionHeight/RegionSizeZ), which var regions do not change. spawn,spawn_mesh,terrain(x, y, cx, cy clamped to [0, 10000]),wandercx/cy clamp, andraycastorigin (+/-1e4): these were already wider than the largest var region (4096), so they are var-safe as they are.terrain,wander(ApplyWander) and roam'sGround()already index the heightmap by its ownWidth/Height.wanderradius max 120 m: this is the size of a creature's loop, not a region bound. The legacy wander samples one height per waypoint (at most 64 steps), so a much larger loop would float over the terrain.roamis the tool for big ranges.- roam journey sizes (
leg_min/leg_max<= 250, flee <= 60, stalk 32 m, set-down search +/-15 cells): these are about the animal, not the region. raycastlength max 1000 m: this is already four times a 256 region. A 768 region's diagonal is 1086 m, so a full corner-to-corner ray needs two casts. That was left as is.liststops at 5000 objects: a 768 region can hold nine times the prims of a 256 one. The cap is a response bound, not a size assumption, but a big built-up var region may hit it.countin the response shows when it did.- Sky, water and wind (
ApplySky,ApplyWater,WindNow): these are region environment settings with no spatial extent. - One Scene per module: WorldBridge binds a single Scene (see
TODO.md, multi-region). The sizes read are that Scene's. A var region is one Scene, so this is correct. Cross-region junctions (a 3x3 beside a 2x2) are a separate multi-region problem and are out of scope here.
Client-side 256 assumptions found in passing were not changed, because the scope here is the module:
tools/terrain_restore.py:26computesstep = 256 // len(g)and iteratesrange(0,256,step). It should readstepXandwidthfrom the response.scenes/plant_grove.pyhascx, cy = 128, 128.tools/wb.pydocuments "64x64".- The MCP
world_heightmaptool (mcp/worldbridge_mcp.py:62) has an empty input schema, so it cannot passgrid. scenes/plant_grove.pyandviewer/render_world.pyalready usestepX/stepYand the grid's length, and are fine.
How to test on a 512 x 512 region
Region config (bin/Regions/Regions.ini, one section per region):
[Ocean512]
RegionUUID = <new uuid>
Location = 1000,1000 ; grid units of 256 m: this region covers 1000..1001 x 1000..1001
SizeX = 512 ; multiple of 256; must equal SizeY (OpenSim forces square)
SizeY = 512
SizeZ = 4096
InternalAddress = 0.0.0.0
InternalPort = 9000
AllowAlternatePorts = False
ExternalHostName = SYSTEMIP
Neighbours of a var region must sit at least SizeX/256 grid units away. For a 3x3 the keys are SizeX = 768 and
SizeY = 768. Do not confuse var regions with the old CombineContiguousRegions "megaregions", which are a different,
retired mechanism. The console prints Region Ocean512 size set to <512,512> on start. Use a var-region-aware viewer.
Checks, in order (each one is a WorldBridge POST):
statereturnssizeX: 512, sizeY: 512.heightmap {}returnswidth 512, stepX 8, samplesX 64, samplesY 64.{"grid":256}gives step 2 and 256 samples.{"grid":512}gives step 1 and 512 samples. Compare a few samples toterrain saveoutput.teleport {"x":480,"y":480,"z":40}should land at (480,480), where it was clamped to 256 before. With no x/y it should land at (256,256).move_object {"uuid":..., "x":500, "y":20}: the object should be at x 500."x":600clamps to 512 (the edge), so prefer 511 when testing.npc_create {}should spawn at (256,256,30). Thennpc_move {"npc":..., "x":450, "y":450}should walk there.chat_send {"message":"hi"}with no avatar is spoken from (256,256,30). Stand within 20 m of the middle to hear it in local chat.raycast {"land":true, "dx":0, "dz":-1, "length":60}should hit land under (256,256).wander {"uuid":...}with no cx/cy should loop around (256,256).roamwithcx:400, cy:400, radius:100: the log should showroam walk grid (400,400) r100: 204x204 cells ...and the creature should walk there. Also tryradius:300(clamped to 180 before, accepted now), and awaterpool across the region. Check that the logged trip grid is <= 509 x 509 and note the build time. It should be seconds, and the region's frame rate should hold while it builds.create_accountfor a throwaway user: the GridUserHomePositionshould be the default region's middle, which is (256,256,26) if that is the 512 region.terrain {"cx":450,"cy":450,"radius":10,"amount":2}should reportchanged> 0. This op was already var-safe and is here as a regression check.
Run the 256-region regression too: heightmap {} is still 64 x 64 at step 4, teleport {} still goes to
(128,128,30), and the live roamers' grid log lines keep the same sizes as before, for example (105,115) r76: 157x157.