Repository navigation
Version 3.0.0: new terrain, water and rivers, maps that match at any zoom, and how it all got faster #28
Filroden
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Version 3.0.0 is the largest release of the module so far, and this post is the longer account of what changed under the hood: how the new terrain, water and rivers work, how regional maps now match the maps they were cropped from, how existing maps are kept exactly as they were, and how generation got faster once all of that was in. The short version is in the changelog. This is for anyone who wants the detail.
The short version
1. Keeping existing maps exactly as they were
Nearly everything in this release changes how terrain is generated, and I did not want anyone to open a map they had spent weeks on and find it different. So the first piece of work was versioning.
Every map now stores a
terrainVersion: 1 for maps saved before this version, and 2 for new maps and for every regional map created from now on. The engines never read the version themselves. A newTerrainVersionmodule turns it into plain values the engines read fromparams.terrain: the resolution scale, how many extra and detail octaves to add, whether the current coastal profile applies, whether the current river rules apply, whether to use exact distances, and aworldframe (the map's zoom, its origin in the top map, and the top map's size). A version 1 map gets values that take the engine down exactly its old path, and the checks for this compare fingerprints of the output against fingerprints taken from 2.6.0.Three other things carry their own version, because they are recorded per item rather than per map:
revisionthey were painted with, andBrushEnginereplays each stroke with its own revision's maths. So the new Smooth and textured Level apply only to strokes painted from this version on, and a map's older strokes replay exactly as before.revision: 2are tectonic features, and the originalTectonicEngineskips them. Original fault lines are untouched.paintSides). Strokes without it replay exactly as before.When a map would be changed by the new rules,
TerrainUpgradeworks out what an update would actually change for that map and lists it in the update dialogue. For regional maps it does this by recomputing the climate with the corrected settings and comparing the biomes, stopping at the first difference (the climate pass is split intoprepareClimate,getMoistureAtandgetTemperatureAtso it can be reused this way). It then applies the update (unsaved, so reloading without saving undoes it), and writes the "don't ask again" choice straight to the saved map as an update of that one field, leaving any unsaved edits alone. There are update kinds for maps whose only change is their fault lines (version 1 Standard and Flat maps with faults) and for maps whose only change is their rivers.A few things do reach every map, because they only change how a map is drawn: the GPU drawing of terrain, water and rivers (section 4), and the fade at the edges of convergent and divergent faults (section 6).
2. One world, many maps
Before this version, a regional map was generated in its own pixels. That worked for Standard maps, but everything else drifted: Guided coastlines were built from the land masks inside the crop only, fault noise was read at a different scale, faults were a quarter as wide on a x4 crop, rain shadows were placed too far downwind, and so on. The fix for all of these was the same idea: work everything out in the pixels of the top map.
The world frame.
RegionalExtractorrecords on every regional map its zoom and its corner in the top map, compounded through crops of crops. Noise that has to match between a parent and its regional maps is read at(originX + x / zoom, originY + y / zoom), in pixels of a 1,000 pixel map, so the same world feature is the same size whatever the map's resolution. On a map that was never cropped this is the map's own pixels, so a top map's output is unchanged.Detail as you zoom in. A regional map has more pixels per piece of the world, so it can show finer detail. The noise function (
#fbm) accepts extra octaves, weighted so the base octaves keep their shape, and a regional map gets as many extra octaves as its zoom allows. Terrain (except on Flat maps), moisture and temperature all gain them, so coastlines and biome borders get more intricate while the world keeps its shape.Coastlines. Guided terrain needs the distance from every point to the coast, and a regional map may have no coast inside it at all. The coast distance field is now built over the map's footprint in the top map plus a margin, at up to one cell per regional pixel (capped by
GENERATION.COAST_FIELD_MAX_CELLS), and a Guided regional map receives all of its parent's land masks, including the ones outside the crop. So a regional map sees the same coastlines as its parent even when none lies inside the crop.Rain shadows. The wind's reach on a regional map was multiplied by the zoom twice, and also depended on the crop's shape. It is now set from the top map's shape. The second problem was at the edges: moisture looks back upwind, and near a regional map's left or right edge that point is off the map, so the map repeated its own edge column and a mountain range just outside it cast no rain shadow into it. A regional map now saves an
upwindMargin(newUpwindMarginmodule): strips of the parent's final elevation either side of the crop, one parent wind reach wide, at the parent's resolution and packed to 16 bits. Strip columns beyond the parent's own edges come from the parent's own record, so crops of crops reach back to the top map.getMoistureAtreads it only when the upwind point is off the map, so a map without one is unchanged. On a x8 crop, biome agreement with the parent in the edge strips went from 76.9% to 91.7%, against 90.0% in the interior.Grids. Regional maps were rounded to whole grid cells, but the crop's corner was never placed on the parent's grid, and Foundry always starts a scene's grid at its top-left corner. So the regional grid never lined up with the parent's, the rounding changed the width you asked for, and rounding the height separately stretched or squeezed the latitude range against the terrain.
RegionalExtractor.planCropnow snaps the crop's top-left corner to the parent's grid pattern (a square cell corner, or the point where a hex pattern repeats every two rows or columns), sets the zoom to exactly regional grid ÷ parent grid, keeps the requested width, and sizes the height at the same zoom. The canvas crop snaps when you let go of it, so the outline shows the map you will get.Faults, brushes and rivers.
TectonicEngine.applyTectonicFaultstakes the frame and reads all fault noise in top map pixels, with hotspot spacing multiplied by the zoom.RegionalExtractormultiplies each fault's thickness by the zoom and keeps any fault whose width reaches into the crop (before, a fault was kept only if one of its points lay inside). Version 1 maps always read fault noise in their own pixels, and updating a version 1 regional map widens its faults by its zoom (TerrainVersion.planUpgraderecords afaultScalethatTerrainUpgrade.applyuses). Level, Slope Up and Slope Down strokes take their height from the ground where they start, which a regional map may not have, so each translated stroke records the height it anchored to on the parent (BrushEngine.resolveAnchors). Translated slope strokes also record theirscale(the compounded zoom), and each step's rise is divided by it, because the replayed stroke's steps are that many times longer in map pixels. River sources are placed in world coordinates (section 5), and every river crossing into the crop gets a spring with aninflow, so it enters as wide as it was on the parent.3. Guided v2 and Advanced (Tectonics) v2
The coastal profile. On current maps the land and sea are shaped by
#shapeCoastalProfile,#shapeLandand#shapeSeabed(configured byGENERATION.COASTAL_PROFILE). Continental Shelf sets a band of shallow sea, beyond which the seabed falls steeply, and Coastal Plains a band of low land. Both widths wander a little so they do not trace the coast. Continent Scale sets only how quickly the land rises and Ocean Scale how quickly the seabed falls, and neither moves the coastline any more.#measureLandmasseslabels each connected landmass and records how far its middle lies from its coast, so small islands can rise to hills of their own while continents keep broad slopes. On this profile the detail noise is read at each pixel's own position rather than the warped one (the warp stretched the finer octaves into streaks that relief shading made obvious), and the output is no longer clamped to 0 to 1.Mid-ocean ridges come from a coarse ridge field over the whole top map:
#buildRidgeFieldon Guided maps, where the seabed is equally far from two continents (small islands do not create ridges), and#buildPlateRidgeFieldon tectonic maps, where plates pull apart.#ridgeLiftshapes the crest and its narrow rift. The crest wanders and kinks (more octaves than first built, plus the map's detail octaves), and#abyssalHillsadds low hills parallel to the ridge: noise read across the ridge at the distance from the crest, and along it at a slowly changing second coordinate, so each band of hills ends after a while and the next begins. The hills only ever raise the seabed.Advanced (Tectonics) v2.
#generateTectonicV2Topographybuilds a plate model with its own random stream and decides land from continent noise plus each plate's buoyancy. The buoyancy leans towards the land or sea already under a plate, which is why adding one more plate changes only a small part of the map. It then shares the Guided coast field and detail pass. Where plates collide, a range can climb above 100% rather than being flattened at the limit. The originalgenerateTectonicTopographyis unchanged and still used for version 1 maps.Scale. Coastal Fracture, Continent Scale, Ocean Scale and the band widths are measured against a 1,000 pixel map, so the same settings give the same world at any resolution. Updating a Guided map that is not 1,000 pixels across lowers its Coastal Fracture to keep its coastline close to its old shape (under 2% of land and sea changed side in the check).
Faster Guided masks. Land mask ownership used to be a point-in-polygon test for every cell against every mask. It is now filled by scanline (
#fillMaskRow): each row finds where every mask's edges cross it and fills between the sorted crossings, with the same crossing formula and even-odd rule, so the result is identical cell for cell and a detailed coastline takes about as long as a simple one.Random land masks.
LandMaskGeneratormakes a random shape as a turning walk whose turns add up to one full circle, so it closes on itself.RandomLandMapmakes a whole map: Voronoi cells (from d3-delaunay, newly bundled) over the map and its buffer are grown into regions, the largest become land up to a random land share, some coastal cells are swapped to roughen the coast, and the land outlines are traced into masks, largest first.4. Drawing the map on the graphics card
The old map was painted on the CPU into three layers (base, topography and biomes), and every setting change repainted them. The new map is drawn by
TerrainCompositor, a PIXIMeshwith a custom shader, from four map-sized images:ProceduralEngine.paintTerrainAux.The layer switches, biome opacity, relief strength and water settings are shader uniforms, so changing them never repaints anything.
TerrainShadingholds the shader and an identical CPU copy (shade), which is what the checks compare against, and which also builds the 3D view's drape (drape, the shader's steps without relief, worked out once per depth and height value). The oldcolorize,createBaseMapand their ocean, lake and land painters are gone, and the base, topography and biome canvas layers are replaced by oneterrainlayer. The first three images take the place of those three layers' images, and the river image is the only new one. The look is configured byDISPLAY.TERRAIN,DISPLAY.WATERandDISPLAY.SHORELINE.#reliefChange, configured byDISPLAY.RELIEF) is measured per pixel of a 1,000 pixel map, so the same hill is shaded the same at any size or zoom, and flat ground keeps exactly its unshaded colour.WATER.LAKE_DEPTH_SCALE, since lakes are shallow). Seabed Relief and the water's tint, saturation and clarity are saved per map inparams.display(seabedRelief,waterHue,waterSaturation,waterClarity).createContourMapafter the contour lines, around the sea but not lakes.BiomePlacementturns each biome's placement into sides (land, underwater and overwater, as bit flags).BiomeRuleEngine.matchBiomeIdskips rules on a side their biome may not appear on, andProceduralEngine.resolveBiomeLookupresolves a land biome, or an underwater and an overwater biome for water, each by override, rule and default.params.biomeSidesreplacesparams.solidOverWater, and older saves are still read.Scene3D.render3DMap) is now built from the map's own images and settings, so it matches the map, and its texture is mipmapped with anisotropic filtering.5. Rivers
Rivers changed in two halves: how they are traced (on current maps only) and how they are drawn (on every map).
Drawing. New
RiverNetworkturns the traced rivers into drawn channels:RiverNetwork.rasterisepaints the fourth terrain image: per pixel, a distance field to the nearest river's edge, how much of it is frozen, and whether the pixel is a pool (a lake too small or narrow to stop a river, which the river is drawn across). The shader turns the distance field into an edge softened over exactly one screen pixel, whichTerrainCompositorworks out from its transform before each draw, so rivers stay sharp at any zoom.PIXI.Graphics, so a vector repaint no longer rebuilds every river.Tracing on current maps.
#traceRiverrecords which river and step each river joins (a per-pixel owner buffer). A river now joins one it runs beside (#findAdjacentRiver), and going downhill it steps to the lower neighbour closest to a point carried straight down the slope (#followSlope), which is what stops the long straight runs at fixed angles.HydrologyEngine.authoredRiversturns each drawn spline into pixels with an inflow from its Width,#traceAuthoredfollows it from its higher end (stopping at the sea or another river, or tracing on downhill from its end), andRiverNetworkkeeps the drawn part on its line, easing into meanders only beyond it.#carveValleylowers the ground into a valley: the most any point of the line lowers each pixel, falling off with a cosine over a few times the river's radius, subtracted from the ground so its texture survives. Version 1 maps keep the old trench.#fillBasinfloods a river's own earlier pixels instead of skipping them, and limits a lake toHYDROLOGY.MAX_LAKE_AREAsquare baseline pixels, so the largest lake is the same share of the world at any size. A full lake is breached (#breachBasin): the flood's search carries on past the lake, without adding to it, until it reaches ground lower than the surface, and the river cuts the channel the search took. Reaching the map's edge lets the river out there.#isSeatells open sea from an inland hollow below sea level, which a river runs through like a lake.RiverSourcesplaces the procedural springs from the map's seed each time the rivers are traced: candidate places are drawn in the world (so a regional map shares its parent's), and those on high enough, wet enough land are kept. Nothing is stored for them, which is why they now follow your edits and why Spring Altitude and Spring Moisture take effect straight away. The canvas shows them as rings you can delete (which adds a removal pin) or drag (which makes it your own spring).RiverSources.findBakedPinsrecognises the spring pins the old version saved by replaying the old sequence, and they are removed when a map first uses the current rules.6. Tectonic features
New
TectonicFeatureEngine(with each feature's class insrc/generation/tectonics/) applies faults saved withrevision: 2: mountain ranges in three styles, subduction zones, rifts and hotspot chains. The design decision that everything else follows from is that each feature's change is worked out from the ground beneath all of the faults (the base terrain plus brush strokes), and the changes are then added together. So the result does not depend on the order of the faults, and a refresh of one area gives exactly the same result as a whole-map pass.Working out a feature can take a few seconds on a large map, so each feature's change is cached on the app, keyed by the feature's settings (not its name), the map's frame and sea level, and a fingerprint of the ground within the feature's reach. Editing one feature therefore only recalculates that feature, and editing elsewhere only recalculates a feature if the ground under it changed. Line coordinates are measured on a coarse grid along the same Catmull-Rom spline the canvas draws, and noise is read in top map pixels with the map's detail octaves, so a feature matches its parent on a regional map.
RegionalExtractorkeeps any feature whose reach meets the crop, so a hotspot chain whose line passes outside a crop still brings in the volcanoes that reach it.New
StudioCanvas#drawFeatureMarkersdraws symbols along each feature's spline to show which way it runs.Updating a map converts its original faults (
#convertFaults): convergent faults become mountain ranges, divergent faults become rifts, hotspots become hotspot chains, and slip faults are removed.The original convergent and divergent faults also changed slightly on every map: their bell curve was still 1.8% of its height at the fault's edge, which relief shading showed as a line along both sides.
TectonicEngine#faultProfilenow eases the outer part of the width (EDGE_TAPER) down to exactly zero with no slope, so they fade out.7. Brushes: Roughen, Smooth and Level
The surface texture.
ProceduralEngine.generateSurfaceTexturebuilds a fine texture in pixels of a 1,000 pixel map, at each pixel's place in the top map, with extra octaves where there are more pixels to the world. It uses a new#signedFbm, centred on 0 and eased towards ±1 with a hyperbolic tangent rather than clamped, because clamping left flat spots. Getting its size right took a few rounds: the first version's coarsest layer was far wider than the brush, so Roughen appeared to push the terrain about rather than texture it. The texture is worked out in 64 pixel tiles, each the first time a brush stamp needs it. The first Roughen stamp on a 3210 x 2400 map used to build the whole texture (about 3.3 seconds); it now takes about 80 ms.Roughness. The texture is part of the elevation, and a roughness byte per pixel records how much of it each pixel already carries, so going over the same ground again adds nothing, Smooth lowers the roughness as it flattens, and Roughen puts back exactly what was removed.
BrushLayerCachekeeps a roughness buffer beside the brushed terrain (in the undo patches too), and a textured Flat map starts fully rough.Never moving the coast. The texture never moves ground by more than 80% of its height above or depth below sea level, and the new Smooth goes through
BrushEngine#holdCoast, which limits any move towards sea level to the same share. A pixel that 32-bit rounding would carry across sea level is left where it is: the check found 757 pixels crossing after 12 passes without that guard.Smooth used to pull each pixel towards the pixel under the brush centre by at most 2.5% per stamp, so it barely did anything. The new Smooth pulls each pixel towards the average of its surroundings, from a summed-area table built before each stamp. One full-Strength pass removes 99% of fine detail, and the lowest Strength removes 53%.
Roughen replay. Roughen was first replayed stamp by stamp, like the other tools, and that turned out to be the main reason one of my maps took 21 seconds to load: 42 Roughen strokes, scrubbed back and forth with a large brush, made 28,608 stamps and 487 million pixel visits, nearly all of which changed nothing. New
RoughenStrokeworks out each pixel's result directly from its elevation and roughness before the stroke and the highest roughness the stroke asks of it, so the result no longer depends on how the stamps overlap. Live painting keeps the pre-stroke values of the pixels it touches, and a replay finds each pixel's nearest stamp along each straight run of stamps and writes it once. Live painting and replay are bit-identical, and in my measurements outside Foundry the replay of those strokes went from about 12 seconds to one or two.BrushGeometryholds the stamp spacing, stamp placement andexactHypotshared by both.8. Speed
With everything above in, v3 had used up most of the speed gained in 2.5.0, so the last piece of work before release was a performance pass on loading and generation. These are the full render times in Foundry on my machine, on the second load of a session:
Where the time went. On the test map, two thirds of the replay was Roughen (section 7). On the tectonic map, 43% of the time was simplex noise (up to about 46 octaves per sea pixel, including coastline noise worked out on nearly half the pixels where it was then multiplied by zero) and 36% was the jump flood that finds the nearest coast (14 passes over 9.6 million cells, with a method call per neighbour and a new 77 MB grid per pass).
Changes that give exactly the same output:
Changes to unreleased v3 maths (so no saved map is affected):
Math.powfor the fourth power.Using every core. In 2.5.0 I tested Web Workers and dropped them, because they did nothing for editing: you still have to wait for the result of your stroke. Generation is different. A whole-map pass where every pixel depends only on its position and data prepared beforehand can be split into bands of rows and worked out on several cores at once, and the result is the same bit for bit however the rows are divided. New
GenerationWorkersdoes this for the per-pixel pass of Guided and current tectonic terrain, Standard terrain, the climate, and the surface texture under every stroke that lays it:detailJobForRows), and a band that reads outside them is refused rather than returned with holes.Painting the layers. After that, painting the map's pixel layers (relief, depth and height, biomes, underwater biomes and contours) was 1.2 to 2.1 seconds of a load on my maps, and the same again whenever something repaints the whole map (changing the contour interval or a biome's colour, or a brush stroke that moves the map's highest or lowest point). Each painter's pixel depends only on the pixels directly around it, so new
LayerPaintingshares a whole-map repaint between the workers too, each band with a thin halo of rows either side. The painters now take 0.33 to 0.54 seconds on those maps, and those repaints feel instant. Repainting a small area (a brush stroke) stays on the main thread, and two situations needed care: an area repainted while a shared repaint is still running is painted again once the shared repaint's bands are in, so newer paint is never lost, and a newer whole-map repaint drops an older one's bands as they arrive.What I did not do. The largest remaining cost of loading a map with thousands of strokes is replaying the older strokes (4.6 to 7.8 seconds on my two big maps). The only way to remove it would be to cache the base terrain and brushed terrain in the browser's storage, about 60 to 80 MB per large map, and I decided against asking every user to give up that much disk space. Tracing rivers on every refresh (about 78 ms of a brush refresh) could also be trimmed, but not by enough for anyone to notice.
9. Keeping large maps responsive
Using v3 on my largest map showed up some problems that had been hiding:
PIXI.Textand reloading every pin icon (pin sprites were destroyed together with their shared textures). Canvas memory lives outside the JavaScript heap, so the browser did not hurry to free it, and memory climbed from 1.8 GB to 7.3 GB until the graphics context was lost and the canvas went black. The live brush no longer touches the vector layers, and pin sprites are destroyed without their shared textures.RefreshQueueruns refreshes one at a time. Each kind (terrain, changed terrain, climate, rivers and canvas) can wait at most once, and a request for a kind already waiting is merged into it (their areas combined) and shares its result.10. Interface plumbing
templates/parts/shared-range.hbspartial andRangeDisplayhelper keep each slider's value display and fill in step, through one document listener and arenderApplicationV2hook. They replace inlineoninputhandlers, per-dialogue listeners, and a listener inMapStudioAppthat was added again on every render. A range withdata-defaultresets to it on double-click in any of the module's windows.<details>elements laid out with::details-content. Collapsed sections are remembered for the session, and a tool panel too short for its contents now scrolls instead of cutting them off.CanvasTransformsworks out a whole-shape move from the starting positions (so there is no drift over many moves), clamped as a whole so the outline never changes, and wheel resizing in whole steps of the dialogue slider's grid.normalizeAnglekeeps angles between -180 and 180 degrees.CanvasHintspicks the hint list from the 3D view, the tool, its mode, the crop, the reference image and the held item. The undo and redo preview, zoom to feature and the shape drag share one highlight box.LabelWidthstores a label'smaxCharsinstead of a pixel width. The wrap width ismaxCharstimes the width of a bold "0" in the label's font (measured once per font, and not cached while a web font is still loading) times its size. Pixel widths are converted on load, and in imported settings files, rounded up.11. Code quality and licences
Before release I went through the ESLint and SonarQube reports. The ESLint warnings are cleared, mostly unused arguments on action handlers (now named with a leading underscore), and the Foundry globals the export uses are declared. The most complex interface functions (the undo and redo preview, the undo and redo step itself, the canvas pointer handlers, grid drawing and scene export) are split into small single-purpose helpers. I checked each against the old code by running both side by side on generated inputs (thousands of random undo histories, pointer states and exports, including failure paths), and they give identical results, calls included. The code SonarQube flags that is deliberate (for example
!(value > 0)checks, which reject a missing or NaN value where the "equivalent"value <= 0would let it through, and the long per-pixel loops that are kept whole for speed) now has a comment saying why, and a// NOSONARmarker.d3-delaunay 6.0.4 is now bundled (with delaunator 5.1.0 and robust-predicates 3.0.3) for the random land maps. Every licence file is now Markdown, since the release script only packs certain file types and the extensionless licence files were being left out of the zip. The README lists every third-party library and icon set with its licence.
Smaller changes, for completeness
config.js:GENERATION.SURFACE_TEXTUREandGENERATION.FLAT_HEIGHT(a value that used to be written into the code),TECTONICS.FEATURES,RANDOM_LAND_MASKandRANDOM_LAND_MAP,UI.CANVAS_HINTS,UI.HIGHLIGHT,UI.ROTATIONandUI.WHEEL_RESIZE.DECORATION_SCALE.UI.WHEEL.SCALE_FACTOR,RiverStepsand theDISPLAY.THREE_Driver settings are removed, as are the 3D view'sTHREE.Linerivers.HYDROLOGY.LOOP_LAKE_AREA, a breach search covers up toHYDROLOGY.BREACH_AREA, and a river hemmed in by its own course pools and breaches the same way as any other. Springs come fromRIVER_DENSITY×SPRING_CANDIDATEScandidate places, kept when they liespringAltOffsetabove sea level and are wetter thanspringMoistMin, and dropped under ablock_springpin. The current ones are held inapp.proceduralSpringsand drawn withDISPLAY.PROCEDURAL_SPRING. Spring pins outside the map are no longer traced.TerrainUpgradereports river changes aschangesRiversandchangesChannels.generateSurfaceTextureaccepts bounds, giving every pixel exactly as a whole-map pass does.TerrainVersion.isUpgradeCandidatenow takes the map's faults.generateClimateDataandprepareClimatetake the upwind margin as a new last argument.processTopographyPhaseandprocessClimatePhaseare now asynchronous. The painters take arowOffset, and bands carryPAINT_HALO_ROWSof halo either side.generateTerrain,generateClimateandgenerateFeaturesnow queue a request, with the work itself in private methods that call their later stages directly (never through the queue, which would wait on itself)._repaintCanvashas avectorsoption, and debounced repaints as well as refreshes stop once the window has closed.RangeDisplay.sync. List sections use thefwmb-list-sectionandfwmb-accordion-groupclasses, clicks on controls inside a list heading do not toggle it, and the Scene tool's land mask section no longer usesfwmb-fixed-lists, which hid its overflow inside a panel that already scrolled. Land samplers passed to the ownership grid are objects (at, and optionallyfillRow).StudioCanvasreports what the pointer holds throughonHeldItemChange, and the node hit targets of regions and land masks carry their shape. The canvas hint and map controls share a.fwmb-map-footerrow, the controls are.fwmb-map-control-groupboxes, and every map overlay takes its background and border from--fwmb-map-overlay-backgroundand--fwmb-map-overlay-border. An unused check for anerasePintoolbar button was removed.LabelWidth.migrateMapconverts widths in#ingestMapPayload, and the width inputs are renamed (labelMaxChars,styleMaxChars,massMaxChars) and step by 1.How I checked all of this
As in 2.5.0, Foundry itself cannot be driven by an automated test, so the checks are standalone Node scripts that import the module's real engine code and check its output rather than a picture. There are now 27 of them. The main kinds of check are:
RegionalExtractorare compared with their parents: coastlines, ridges, faults (about 1% mean difference on a x4 crop, against about 80% before), features, moisture at the edges, grids, rivers and river sources.What is not covered is the glue that needs Foundry itself. That I tested by hand in Foundry on my own maps, which is a person looking at a result rather than a test.
If a map loads more slowly than you would expect after this update, or something looks wrong, please open an issue with the console summary (press F12 and look for the lines starting
World Map Builder |), your map's size and roughly how many strokes it has.All reactions