Map generator -verbose and -performance flags (#2721)

Resolves #2718

## Description:

Adds go-style error log levels, with an additional ALL log level.

- WARN/ERROR - Only success output
- INFO - Existing output
- DEBUG - New output
- ALL - New output (includes the logs from when removal/performance is
enabled)

In addition

- Add `-verbose` (`-v`), `-log-level`, `-log-removal`, and
`-log-performance` flags to map generator
- No changes to default behavior of `go run .` without the new flags
- excludes test maps from performance warnings (test maps already skip
the removal steps)
- updates readme with the different flags and how they impact the logger

Default run (matches existing)
`go run . >> output.txt 2>&1`

[output.txt](https://github.com/user-attachments/files/24365745/output.txt)

Default run w/ `-verbose` (log level DEBUG)
`go run . -v >> output.txt 2>&1`

[output.txt](https://github.com/user-attachments/files/24365812/output.txt)

Default run w/ `-log-performance`
`go run . -log-performance >> output.txt 2>&1`

[output.txt](https://github.com/user-attachments/files/24365971/output.txt)

Run of just africa w/ all new logging enabled
`go run . -maps=africa -log-level=all >> output.txt 2>&1`

[output.txt](https://github.com/user-attachments/files/24365724/output.txt)


## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

tidwell

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Aaron Tidwell
2026-01-15 17:40:45 -05:00
committed by GitHub
parent 01e682b576
commit 9cd87f8906
4 changed files with 326 additions and 40 deletions
+62 -34
View File
@@ -2,20 +2,25 @@ package main
import (
"bytes"
"context"
"fmt"
"image"
"image/color"
"image/png"
"log"
"math"
"github.com/chai2010/webp"
)
// The smallest a body of land or lake can be, all smaller are removed
const (
// The smallest a body of land or lake can be, all smaller are removed
minIslandSize = 30
minLakeSize = 200
// the recommended max area pixel size for input images
minRecommendedPixelSize = 2000000
maxRecommendedPixelSize = 3000000
// the recommended max number of land tiles in the output bin at full size
maxRecommendedLandTileCount = 3000000
)
// Holds raw RGBA image data for the thumbnail
@@ -94,7 +99,8 @@ type GeneratorArgs struct {
//
// Misc Notes
// - It normalizes map width/height to multiples of 4 for the mini map downscaling.
func GenerateMap(args GeneratorArgs) (MapResult, error) {
func GenerateMap(ctx context.Context, args GeneratorArgs) (MapResult, error) {
logger := LoggerFromContext(ctx)
img, err := png.Decode(bytes.NewReader(args.ImageBuffer))
if err != nil {
return MapResult{}, fmt.Errorf("failed to decode PNG: %w", err)
@@ -107,7 +113,12 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
width = width - (width % 4)
height = height - (height % 4)
log.Printf("Processing Map: %s, dimensions: %dx%d", args.Name, width, height)
logger.Info(fmt.Sprintf("Processing Map: %s, dimensions: %dx%d", args.Name, width, height))
area := width * height
if area < minRecommendedPixelSize || area > maxRecommendedPixelSize {
logger.Debug(fmt.Sprintf("Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize), PerformanceLogTag)
}
// Initialize terrain grid
terrain := make([][]Terrain, width)
@@ -137,16 +148,16 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
}
}
removeSmallIslands(terrain, args.RemoveSmall)
processWater(terrain, args.RemoveSmall)
removeSmallIslands(ctx, terrain, args.RemoveSmall)
processWater(ctx, terrain, args.RemoveSmall)
terrain4x := createMiniMap(terrain)
processWater(terrain4x, false)
processWater(ctx, terrain4x, false)
terrain16x := createMiniMap(terrain4x)
processWater(terrain16x, false)
processWater(ctx, terrain16x, false)
thumb := createMapThumbnail(terrain4x, 0.5)
thumb := createMapThumbnail(ctx, terrain4x, 0.5)
webp, err := convertToWebP(ThumbData{
Data: thumb.Pix,
Width: thumb.Bounds().Dx(),
@@ -156,9 +167,20 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
return MapResult{}, fmt.Errorf("failed to save thumbnail: %w", err)
}
mapData, mapNumLandTiles := packTerrain(terrain)
mapData4x, numLandTiles4x := packTerrain(terrain4x)
mapData16x, numLandTiles16x := packTerrain(terrain16x)
mapData, mapNumLandTiles := packTerrain(ctx, terrain)
mapData4x, numLandTiles4x := packTerrain(ctx, terrain4x)
mapData16x, numLandTiles16x := packTerrain(ctx, terrain16x)
logger.Debug(fmt.Sprintf("Land Tile Count (1x): %d", mapNumLandTiles))
logger.Debug(fmt.Sprintf("Land Tile Count (4x): %d", numLandTiles4x))
logger.Debug(fmt.Sprintf("Land Tile Count (16x): %d", numLandTiles16x))
if mapNumLandTiles == 0 {
return MapResult{}, fmt.Errorf("Map has 0 land tiles")
}
if mapNumLandTiles > maxRecommendedLandTileCount {
logger.Debug(fmt.Sprintf("Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag)
}
return MapResult{
Map: MapInfo{
@@ -242,8 +264,9 @@ func createMiniMap(tm [][]Terrain) [][]Terrain {
// It marks Land tiles as shoreline if they neighbor Water, and Water tiles as
// shoreline if they neighbor Land.
// Returns a list of coordinates for all shoreline Water tiles found.
func processShore(terrain [][]Terrain) []Coord {
log.Println("Identifying shorelines")
func processShore(ctx context.Context, terrain [][]Terrain) []Coord {
logger := LoggerFromContext(ctx)
logger.Info("Identifying shorelines")
var shorelineWaters []Coord
width := len(terrain)
height := len(terrain[0])
@@ -280,8 +303,9 @@ func processShore(terrain [][]Terrain) []Coord {
// processDistToLand calculates the distance of water tiles from the nearest land.
// It uses a Breadth-First Search (BFS) starting from the shoreline water tiles.
// The distance is stored in the Magnitude field of the Water tiles.
func processDistToLand(shorelineWaters []Coord, terrain [][]Terrain) {
log.Println("Setting Water tiles magnitude = Manhattan distance from nearest land")
func processDistToLand(ctx context.Context, shorelineWaters []Coord, terrain [][]Terrain) {
logger := LoggerFromContext(ctx)
logger.Info("Setting Water tiles magnitude = Manhattan distance from nearest land")
width := len(terrain)
height := len(terrain[0])
@@ -362,8 +386,9 @@ func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord {
// It finds all connected water bodies and marks the largest one as Ocean.
// If removeSmall is true, lakes smaller than minLakeSize are converted to Land.
// Finally, it triggers shoreline identification and distance-to-land calculations.
func processWater(terrain [][]Terrain, removeSmall bool) {
log.Println("Processing water bodies")
func processWater(ctx context.Context, terrain [][]Terrain, removeSmall bool) {
logger := LoggerFromContext(ctx)
logger.Info("Processing water bodies")
visited := make(map[string]bool)
type waterBody struct {
@@ -408,13 +433,14 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
for _, coord := range largestWaterBody.coords {
terrain[coord.X][coord.Y].Ocean = true
}
log.Printf("Identified ocean with %d water tiles", largestWaterBody.size)
logger.Info(fmt.Sprintf("Identified ocean with %d water tiles", largestWaterBody.size))
if removeSmall {
// Remove small water bodies
log.Println("Searching for small water bodies for removal")
logger.Info("Searching for small water bodies for removal")
for w := 1; w < len(waterBodies); w++ {
if waterBodies[w].size < minLakeSize {
logger.Debug(fmt.Sprintf("Removing small lake at %d,%d (size %d)", waterBodies[w].coords[0].X, waterBodies[w].coords[0].Y, waterBodies[w].size), RemovalLogTag)
smallLakes++
for _, coord := range waterBodies[w].coords {
terrain[coord.X][coord.Y].Type = Land
@@ -422,15 +448,14 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
}
}
}
log.Printf("Identified and removed %d bodies of water smaller than %d tiles",
smallLakes, minLakeSize)
logger.Info(fmt.Sprintf("Identified and removed %d bodies of water smaller than %d tiles", smallLakes, minLakeSize))
}
// Process shorelines and distances
shorelineWaters := processShore(terrain)
processDistToLand(shorelineWaters, terrain)
shorelineWaters := processShore(ctx, terrain)
processDistToLand(ctx, shorelineWaters, terrain)
} else {
log.Println("No water bodies found in the map")
logger.Info("No water bodies found in the map")
}
}
@@ -465,7 +490,8 @@ func getArea(x, y int, terrain [][]Terrain, visited map[string]bool) []Coord {
// removeSmallIslands identifies and removes small land masses from the terrain.
// If removeSmall is true, any removed bodies are converted to Water.
func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
func removeSmallIslands(ctx context.Context, terrain [][]Terrain, removeSmall bool) {
logger := LoggerFromContext(ctx)
if !removeSmall {
return
}
@@ -501,6 +527,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
for _, body := range landBodies {
if body.size < minIslandSize {
logger.Debug(fmt.Sprintf("Removing small island at %d,%d (size %d)", body.coords[0].X, body.coords[0].Y, body.size), RemovalLogTag)
smallIslands++
for _, coord := range body.coords {
terrain[coord.X][coord.Y].Type = Water
@@ -509,8 +536,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
}
}
log.Printf("Identified and removed %d islands smaller than %d tiles",
smallIslands, minIslandSize)
logger.Info(fmt.Sprintf("Identified and removed %d islands smaller than %d tiles", smallIslands, minIslandSize))
}
// packTerrain serializes the terrain grid into a byte slice.
@@ -521,7 +547,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
// - Bits 0-4: Magnitude (0-31). For Water, this is (Distance / 2).
//
// Returns the packed data and the count of land tiles.
func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
func packTerrain(ctx context.Context, terrain [][]Terrain) (data []byte, numLandTiles int) {
width := len(terrain)
height := len(terrain[0])
packedData := make([]byte, width*height)
@@ -553,15 +579,16 @@ func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
}
}
logBinaryAsBits(packedData, 8)
logBinaryAsBits(ctx, packedData, 8)
return packedData, numLandTiles
}
// createMapThumbnail generates an RGBA image representation of the terrain.
// It scales the map dimensions based on the provided quality factor.
// Each pixel's color is determined by the terrain type and magnitude via getThumbnailColor.
func createMapThumbnail(terrain [][]Terrain, quality float64) *image.RGBA {
log.Println("Creating thumbnail")
func createMapThumbnail(ctx context.Context, terrain [][]Terrain, quality float64) *image.RGBA {
logger := LoggerFromContext(ctx)
logger.Info("Creating thumbnail")
srcWidth := len(terrain)
srcHeight := len(terrain[0])
@@ -664,7 +691,8 @@ func getThumbnailColor(t Terrain) RGBA {
// logBinaryAsBits logs the binary representation of the first 'length' bytes of data.
// It is a helper function for debugging packed terrain data.
func logBinaryAsBits(data []byte, length int) {
func logBinaryAsBits(ctx context.Context, data []byte, length int) {
logger := LoggerFromContext(ctx)
if length > len(data) {
length = len(data)
}
@@ -673,7 +701,7 @@ func logBinaryAsBits(data []byte, length int) {
for i := 0; i < length; i++ {
bits += fmt.Sprintf("%08b ", data[i])
}
log.Printf("Binary data (bits): %s", bits)
logger.Info(fmt.Sprintf("Binary data (bits): %s", bits))
}
// createCombinedBinary combines the info JSON, map data, and mini-map data into a single binary buffer.