Map Chunk Loader

Renders regions of the world straight from the cache: minimap images, terrain height maps and collision maps.

A region is 64 by 64 tiles drawn at SCALE pixels each. Rendering one means blending its ground colours - which reaches into the neighbouring regions, so those are loaded too - drawing the walls, doors and map scene symbols on top, and then compositing the planes each tile is visible on. Finished planes and tile classifications are cached per region, so panning back over ground already drawn is cheap.

Example:

CacheReader := TCacheReader.Create();
CacheReader.Indices[ECacheIndex.MAPS].Cache();
chunkLoader.MapData.Load();
chunkLoader.GetChunksImage([48, 48, 52, 52], 0).Show();

TileShapes.Setup

procedure TTileShapes.Setup();

Applies every rotation to every tile shape up front, so that drawing an overlaid tile reads one value per pixel instead of a rotation and then a shape.

Masks[shape * 4 + rotation] is the 16 pixel mask of that combination: 1 where the overlay is drawn and 0 where the underlay shows through. There are only 13 shapes and 4 rotations, so the whole table is 52 masks. Called once at startup by cache.simba.


MapChunkLoader.EnsurePlaneCache

procedure TMapChunkLoader.EnsurePlaneCache(regionIndex: Integer);

Makes room in the render caches for a region that has just been loaded.

Nothing is rendered here - the slots start empty and are filled the first time each plane of that region is asked for. Regions are never evicted, so a chunk is only ever built once.


MapChunkLoader.EnsureColorsCurrent

procedure TMapChunkLoader.EnsureColorsCurrent();

Throws away everything that was coloured through the old palette, if TColorUtils.SetBrightness has been called since it was built.

ColorUtils cannot reach in and do this itself - it is defined long before anything that draws, and there may be more than one chunk loader - so it counts its changes in Revision and this compares against the revision these caches were filled at.

What goes is the rendered planes and the overlay colours, both of which are palette colours, and the tile classifications with them. The classifications do not depend on the palette, but TMapChunkLoader.GetChunkImage only rebuilds a missing plane while it is classifying the tiles that ask for it - so dropping planes and keeping classifications would leave it compositing planes that are no longer there. They are one pass over 4096 tiles to rebuild, against the plane renders being thrown away regardless.

The icon lists and the loaded regions themselves are untouched: neither depends on the palette, and they are the expensive half.


MapChunkLoader.ConvertLocal

function TMapChunkLoader.ConvertLocal(d: Integer): Integer; static;

Wraps a coordinate that may have run off the edge of a region back into 0 to 63.

Blending reads a few tiles past the region it is drawing, into whichever neighbour is there, and this is what turns the out of range coordinate into an index into that neighbour.


MapChunkLoader pixel buffers

Every pixels matrix built here is row-major, indexed [y, x], matching how Simba indexes a TIntegerMatrix. x and y below are tile coordinates, so the square each tile occupies is SCALE pixels wide and tall.

The minimap’s matrices hold ColorUtils.Palette entries and the collision matrices the TColorUtils.COLLISION_ colours, both alpha, red, green, blue from the top byte down, so both reach an image through TImage.FromData without being converted. The minimap composites its planes into one flat buffer first; the collision image is copied out of its matrix a row at a time.

The square goes through TIntegerMatrix.Fill rather than a loop over its SCALE * SCALE pixels. That looks like the long way round for 16 pixels, but Fill is native while a loop here is not, and measuring the two puts the native call ahead by about a factor of two - which matters, since this runs for most of the 4096 tiles of every plane of every region.


MapChunkLoader.BlitMapDecoration

procedure TMapChunkLoader.BlitMapDecoration(var pixels: TIntegerMatrix; x, y: Integer; constref obj: TObjectDefinition);

Draws an object’s map scene sprite - the staircase, altar or furnace symbol the minimap uses in place of the object itself.

The sprite is centred on the object’s footprint and clipped to the chunk. Its pixels come from TMapDataLoader.PrepareForRendering, which has already swapped them into the buffer’s channel order and flattened a transparent pixel to 0, so this is a copy that skips zeroes.


MapChunkLoader.DrawObjectAtTile

procedure TMapChunkLoader.DrawObjectAtTile(var pixels: TIntegerMatrix; constref loc: TWorldLocation; x, y: Integer);

Draws one object onto a minimap plane, if it is the kind of object the minimap shows at all - walls, doors and the objects that carry a map scene symbol.

Walls are drawn as a line along whichever tile edge their orientation points at, corner walls along the two edges they occupy, corner posts as a single pixel, and diagonal walls across the tile. y is flipped on the way in because the map is drawn with north at the top while the world counts y upwards.


MapChunkLoader.DrawObjectsOnPlane

procedure TMapChunkLoader.DrawObjectsOnPlane(var pixels: TIntegerMatrix; constref region: TRegion; z: Integer);

Draws every object of a region that sits on plane z.


MapChunkLoader.DrawMapIcons

procedure TMapChunkLoader.DrawMapIcons(img: TImage; regionIdx, z, offsetX, offsetY: Integer);

Draws the area icons of the region at regionIdx in RegionLoader.Regions on top of an already rendered map, offset by offsetX, offsetY within img.

The region’s render caches have to exist already, which TMapChunkLoader.GetChunkImage sees to.

Icons go on last, and onto the whole image rather than a single chunk, so that one sitting on a chunk boundary is not cut in half - which is why TMapChunkLoader.GetChunksImage renders its chunks with icons turned off and then comes back for them.

Only a handful of a region’s several thousand locations carry an icon at all, so which ones they are is worked out once per region and plane and kept - packed one integer per icon, tile x and y in the low twelve bits and the area’s index above them. Panning back over a region then costs the drawing alone rather than another walk of every location on it.


MapChunkLoader.BuildRegionPlane

function TMapChunkLoader.BuildRegionPlane(constref region: TRegion; z: Integer): TIntegerMatrix;

Renders one plane of one region into a pixel matrix: ground colours first, then the objects the minimap draws on top of them.

Underlay colours are blended over a BLEND tile radius so that terrain types fade into each other instead of meeting at a hard edge. That blend is a running sum: rather than re-adding a whole neighbourhood per tile, each step adds the row entering the window and subtracts the one leaving it. Where the window runs past the edge of the region it continues into the neighbouring one, which is why TRegionLoader.LoadRegionAndNeighbors loads four neighbours as well.

Overlays are drawn over the result in one of 13 shapes at one of 4 rotations - a full tile, a half, a corner, and so on - which is how the map gets diagonal shorelines and path edges out of square tiles.

The result is cached by TMapChunkLoader.GetChunkImage, so this only runs once per region and plane.


MapChunkLoader.CompositePlanes

procedure TMapChunkLoader.CompositePlanes(var pixels: TIntegerArray; constref planes: array of TIntegerMatrix; constref tileCodes: TIntegerArray); static;

Flattens the planes a chunk uses into pixels, following the per-tile plan in tileCodes.

pixels is one row-major IMAGE_SIZE * IMAGE_SIZE buffer rather than a matrix, so it is a single block of memory that TImage.FromData can read as it is.

It copies runs of tiles that share a plan in one go, because aliasing a row out of a plane costs far more than the handful of pixel copies it saves. The runs are found once per tile row and reused for each of the SCALE pixel rows that tile row covers, since those rows all share it. Within a run the topmost plane is copied over with a single Move and the ones below it only fill in whatever the topmost left clear.


MapChunkLoader.GetChunkImage

function TMapChunkLoader.GetChunkImage(regionX, regionY, z: Integer; drawIcons: Boolean = True): TImage;

Renders one region as a IMAGE_SIZE by IMAGE_SIZE minimap image. Anywhere nothing is drawn comes out opaque black.

Each tile picks up to 3 planes to composite, topmost first. Those are packed into a single integer per tile - the low nibble is how many planes the tile uses and each nibble above it holds one plane index - so classifying every tile costs one TIntegerArray rather than one array per plane slot.


HeightMap.ToImage

function THeightMap.ToImage(): TImage;

The height map as a greyscale image, black at ground level and white at the highest terrain the game uses.

The map is indexed [x, y], the way TMapChunkLoader.GetChunksHeightMap builds it, so the image is as wide as the outer array is long and as tall as each column.

Each height’s grey is read out of ColorUtils.HeightColors rather than worked out again, and the pixels are laid out into one buffer and handed over in a single call, rather than written one at a time through TImage.Pixel - a property write per pixel is most of what building the image would otherwise cost.


MapChunkLoader.GetChunkHeightMap

function TMapChunkLoader.GetChunkHeightMap(regionX, regionY, z: Integer): THeightMap;

The terrain height of every tile of one region on plane z, one entry per tile rather than per pixel.

A bridge tile reports the height of the surface walked on rather than the one underneath it: like the client, when plane 1 flags a tile as a bridge, every plane below 3 reads the height of the plane above it there.


MapChunkLoader.GetChunksImage

function TMapChunkLoader.GetChunksImage(bounds: TBox; z: Integer): TImage;

Renders a rectangle of regions into one image, in region coordinates.

The chunks are drawn without their icons and the icons are added afterwards over the finished image, so an icon straddling a chunk boundary stays whole.

Example:

img := chunkLoader.GetChunksImage([48, 48, 52, 52], 0);
img.Show();

MapChunkLoader.GetChunksHeightMap

function TMapChunkLoader.GetChunksHeightMap(bounds: TBox; z: Integer): THeightMap;

TMapChunkLoader.GetChunkHeightMap across a rectangle of regions, stitched into one map laid out the way the image is - north at the top.


CollisionMap.ToImage

function TCollisionMap.ToImage(): TImage;

The collision map as an image: white where a tile can be walked on, black where it is blocked, grey where an object stands on it.

The map is indexed [y, x], the way TMapChunkLoader.GetChunksCollisionMap builds it.

The pixels are laid out into one buffer and handed over in a single call, rather than written one at a time through TImage.Pixel - a property write per pixel is most of what building the image would otherwise cost.


MapChunkLoader.DrawCollisionEdge

procedure TMapChunkLoader.DrawCollisionEdge(var pixels: TIntegerMatrix; drawX, drawY, edge, color: Integer); static;

Draws one edge of the tile whose top-left pixel is drawX, drawY. edge is 0 for the left edge, 1 for the top, 2 for the right and 3 for the bottom, which is also the order a wall’s orientation uses.

Nothing is drawn if that tile is outside the chunk, which happens to walls belonging to a neighbouring region.


MapChunkLoader.DrawCollisionWall

procedure TMapChunkLoader.DrawCollisionWall(var pixels: TIntegerMatrix; drawX, drawY, orientation, color: Integer; isDoor: Boolean); static;

Draws a wall on the edge its orientation points at.

A door blocks the tile it swings into rather than the one it stands on, so it is drawn one edge further round, and a south-facing one lands on the row below the tile entirely - which can be the top row of the chunk to the south.


MapChunkLoader.DrawCollisionObjectAtTile

procedure TMapChunkLoader.DrawCollisionObjectAtTile(var pixels: TIntegerMatrix; constref loc: TWorldLocation; x, y: Integer);

Marks the tiles one object blocks on a collision map.

Walls and corners block the edge they sit on - or the next edge round, if the object is an openable door, since a closed door blocks the tile it opens into rather than the one it stands on. Everything larger blocks its whole footprint, rotated with the object, and does not paint over a wall that was already marked there.

x, y are relative to the chunk being drawn and can fall outside it: an object standing in a neighbouring region is passed in too, and only the part of it that reaches into this chunk is drawn, the same way the client clips a footprint to its collision map.


MapChunkLoader.DrawCollisionLocation

procedure TMapChunkLoader.DrawCollisionLocation(var pixels: TIntegerMatrix; constref region: TRegion; constref loc: TWorldLocation; z, offsetX, offsetY: Integer);

Marks one of region’s objects on the collision map of plane z, if it is on that plane.

On the ground plane a bridge tile takes its objects from plane 1, since that is the surface being walked on, and tiles the plane says are not rendered are skipped.

offsetX, offsetY is where region sits relative to the chunk being drawn, in tiles: 0, 0 for the chunk itself, or -64, 0 for the region west of it.


MapChunkLoader.DrawCollisionObjectsOnPlane

procedure TMapChunkLoader.DrawCollisionObjectsOnPlane(var pixels: TIntegerMatrix; constref region: TRegion; z: Integer);

Marks every object of a region that blocks movement on plane z.


MapChunkLoader.GetRegionOverhangs

function TMapChunkLoader.GetRegionOverhangs(regionIdx: Integer): TIntegerArray;

The indices into a region’s Locations of the objects that can reach into a neighbouring chunk’s collision map: footprints running past the region’s east or north edge, and walls on its bottom row, since a door there can draw on the row below.

Worked out once per region and cached, so drawing a chunk’s neighbours only goes through the few objects on their edges instead of all of them.


MapChunkLoader.DrawCollisionObjects

procedure TMapChunkLoader.DrawCollisionObjects(var pixels: TIntegerMatrix; regionX, regionY, z: Integer);

Marks every object that blocks movement on plane z of the chunk at regionX, regionY.

An object’s position is its south-west tile and its footprint grows east and north, so objects standing in the regions to the west, south and south-west can reach into this chunk, and a door on the bottom row of the region to the north can reach its top row. Those are drawn as well, clipped to the chunk, so the collision has no gaps along chunk boundaries.

The chunk’s own objects go last, so where a neighbour’s footprint meets one of its doors the door is what shows.


MapChunkLoader.GetChunkCollisionMap

function TMapChunkLoader.GetChunkCollisionMap(regionX, regionY, z: Integer): TCollisionMap;

Which tiles of one region are blocked on plane z, one entry per tile: 1 for blocked terrain and 0 for walkable.

This is the terrain flag only - objects are not included. Use TMapChunkLoader.GetChunkCollisionImage for a picture that has them.


MapChunkLoader.GetChunkCollisionImage

function TMapChunkLoader.GetChunkCollisionImage(regionX, regionY, z: Integer): TImage;

One region’s collision drawn at the same scale as its minimap image, with blocked terrain, walls and doors, and the footprints of the objects standing on it.


MapChunkLoader.GetChunksCollisionMap

function TMapChunkLoader.GetChunksCollisionMap(bounds: TBox; z: Integer): TCollisionMap;

TMapChunkLoader.GetChunkCollisionMap across a rectangle of regions, stitched into one map indexed [y, x].


MapChunkLoader.GetChunksCollisionImage

function TMapChunkLoader.GetChunksCollisionImage(bounds: TBox; z: Integer): TImage;

TMapChunkLoader.GetChunkCollisionImage across a rectangle of regions, stitched into one image.