From c72354f7899df59e8243a516d4c3b0b2040c93bc Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Sat, 27 Dec 2025 19:05:42 -0500 Subject: [PATCH 01/17] Add -verbose and -performance flags to map generator --- map-generator/README.md | 6 ++ map-generator/main.go | 7 ++ map-generator/map_generator.go | 114 ++++++++++++++++++++++++--------- 3 files changed, 98 insertions(+), 29 deletions(-) diff --git a/map-generator/README.md b/map-generator/README.md index 896e922abf..cebeca9192 100644 --- a/map-generator/README.md +++ b/map-generator/README.md @@ -41,6 +41,12 @@ To process a subset of maps, pass a comma-separated list: - `../resources/maps//map16x.bin` - 1/16 scale (quarter dimensions) binary map data used for mini-maps. - `../resources/maps//thumbnail.webp` - WebP image thumbnail of the map. +## Command Line Flags + +- `--maps`: Comma-separated list of maps to process. +- `--verbose` or `-v`: Turns on additional logging during the map generation process. +- `--performance`: Adds additional logging checks for performance-based recommendations. + ## Create image.png The map-generator will process your input file at `assets/maps//image.png` to generate the map diff --git a/map-generator/main.go b/map-generator/main.go index 44db462da5..f57737306e 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -13,6 +13,8 @@ import ( // mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument. var mapsFlag string +var verboseFlag bool +var performanceFlag bool // maps defines the registry of available maps to be processed. // Each entry contains the folder name and a flag indicating if it's a test map. @@ -131,6 +133,8 @@ func processMap(name string, isTest bool) error { ImageBuffer: imageBuffer, RemoveSmall: !isTest, // Don't remove small islands for test maps Name: name, + Verbose: verboseFlag, + Performance: performanceFlag && !isTest, }) if err != nil { return fmt.Errorf("failed to generate map for %s: %w", name, err) @@ -246,6 +250,9 @@ func loadTerrainMaps() error { // It parses flags and triggers the map generation process. func main() { flag.StringVar(&mapsFlag, "maps", "", "optional comma-separated list of maps to process. ex: --maps=world,eastasia,big_plains") + flag.BoolVar(&verboseFlag, "verbose", false, "Turns on additional logging during the map generation process") + flag.BoolVar(&verboseFlag, "v", false, "Turns on additional logging (shorthand)") + flag.BoolVar(&performanceFlag, "performance", false, "Adds additional logging checks for performance-based recommendations") flag.Parse() if err := loadTerrainMaps(); err != nil { diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index f27ea0fdaa..eed16ecffe 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -18,6 +18,15 @@ const ( minLakeSize = 200 ) +// the recommended max area pixel size for input images +const ( + minRecommendedPixelSize = 2000000 + maxRecommendedPixelSize = 3000000 +) + +// the recommended max number of land tiles in the output bin at full size +const maxRecommendedLandTileCount = 3000000 + // Holds raw RGBA image data for the thumbnail type ThumbData struct { Data []byte @@ -69,6 +78,14 @@ type GeneratorArgs struct { Name string ImageBuffer []byte RemoveSmall bool + Verbose bool + Performance bool +} + +// LogArgs are the props that logs can define to control when they are shown +type LogArgs struct { + isVerbose bool + isPerformance bool } // GenerateMap is the main map-generator workflow. @@ -95,6 +112,21 @@ 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) { + // create a logger that will prepend the mapname in -verbose mode + // and handle checking against the performance and verbose flags + logger := func(LogArgs LogArgs, format string, v ...interface{}) { + if LogArgs.isVerbose && !args.Verbose { + return + } + if LogArgs.isPerformance && !args.Performance { + return + } + if args.Verbose { + log.Printf("[%s] %s", args.Name, fmt.Sprintf(format, v...)) + } else { + log.Printf(format, v...) + } + } img, err := png.Decode(bytes.NewReader(args.ImageBuffer)) if err != nil { return MapResult{}, fmt.Errorf("failed to decode PNG: %w", err) @@ -107,7 +139,15 @@ 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) + // only perform this check if the performance flag is enabled + if args.Performance { + area := width * height + if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { + logger(LogArgs{isPerformance: true}, "⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize) + } + } + + logger(LogArgs{}, "Processing Map: %s, dimensions: %dx%d", args.Name, width, height) // Initialize terrain grid terrain := make([][]Terrain, width) @@ -137,16 +177,16 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { } } - removeSmallIslands(terrain, args.RemoveSmall) - processWater(terrain, args.RemoveSmall) + removeSmallIslands(terrain, args.RemoveSmall, logger) + processWater(terrain, args.RemoveSmall, logger) terrain4x := createMiniMap(terrain) - processWater(terrain4x, false) + processWater(terrain4x, false, logger) terrain16x := createMiniMap(terrain4x) - processWater(terrain16x, false) + processWater(terrain16x, false, logger) - thumb := createMapThumbnail(terrain4x, 0.5) + thumb := createMapThumbnail(terrain4x, 0.5, logger) webp, err := convertToWebP(ThumbData{ Data: thumb.Pix, Width: thumb.Bounds().Dx(), @@ -156,9 +196,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(terrain, logger) + mapData4x, numLandTiles4x := packTerrain(terrain4x, logger) + mapData16x, numLandTiles16x := packTerrain(terrain16x, logger) + + logger(LogArgs{isVerbose: true}, "Land Tile Count (1x): %d", mapNumLandTiles) + logger(LogArgs{isVerbose: true}, "Land Tile Count (4x): %d", numLandTiles4x) + logger(LogArgs{isVerbose: true}, "Land Tile Count (16x): %d", numLandTiles16x) + + if mapNumLandTiles == 0 { + logger(LogArgs{isPerformance: true}, "⚠️ Map has 0 land tiles") + } + if mapNumLandTiles > maxRecommendedLandTileCount { + logger(LogArgs{isPerformance: true}, "⚠️ Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount) + } return MapResult{ Map: MapInfo{ @@ -242,8 +293,8 @@ 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(terrain [][]Terrain, logger Logger) []Coord { + logger(LogArgs{}, "Identifying shorelines") var shorelineWaters []Coord width := len(terrain) height := len(terrain[0]) @@ -280,8 +331,8 @@ 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(shorelineWaters []Coord, terrain [][]Terrain, logger Logger) { + logger(LogArgs{}, "Setting Water tiles magnitude = Manhattan distance from nearest land") width := len(terrain) height := len(terrain[0]) @@ -362,8 +413,8 @@ 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(terrain [][]Terrain, removeSmall bool, logger Logger) { + logger(LogArgs{}, "Processing water bodies") visited := make(map[string]bool) type waterBody struct { @@ -408,13 +459,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(LogArgs{}, "Identified ocean with %d water tiles", largestWaterBody.size) if removeSmall { // Remove small water bodies - log.Println("Searching for small water bodies for removal") + logger(LogArgs{}, "Searching for small water bodies for removal") for w := 1; w < len(waterBodies); w++ { if waterBodies[w].size < minLakeSize { + logger(LogArgs{isVerbose: true}, "Removing small lake at %d,%d (size %d)", waterBodies[w].coords[0].X, waterBodies[w].coords[0].Y, waterBodies[w].size) smallLakes++ for _, coord := range waterBodies[w].coords { terrain[coord.X][coord.Y].Type = Land @@ -422,15 +474,15 @@ func processWater(terrain [][]Terrain, removeSmall bool) { } } } - log.Printf("Identified and removed %d bodies of water smaller than %d tiles", + logger(LogArgs{}, "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(terrain, logger) + processDistToLand(shorelineWaters, terrain, logger) } else { - log.Println("No water bodies found in the map") + logger(LogArgs{}, "No water bodies found in the map") } } @@ -465,7 +517,7 @@ 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(terrain [][]Terrain, removeSmall bool, logger Logger) { if !removeSmall { return } @@ -501,6 +553,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) { for _, body := range landBodies { if body.size < minIslandSize { + logger(LogArgs{isVerbose: true}, "Removing small island at %d,%d (size %d)", body.coords[0].X, body.coords[0].Y, body.size) smallIslands++ for _, coord := range body.coords { terrain[coord.X][coord.Y].Type = Water @@ -509,7 +562,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) { } } - log.Printf("Identified and removed %d islands smaller than %d tiles", + logger(LogArgs{}, "Identified and removed %d islands smaller than %d tiles", smallIslands, minIslandSize) } @@ -521,7 +574,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(terrain [][]Terrain, logger Logger) (data []byte, numLandTiles int) { width := len(terrain) height := len(terrain[0]) packedData := make([]byte, width*height) @@ -553,15 +606,15 @@ func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) { } } - logBinaryAsBits(packedData, 8) + logBinaryAsBits(packedData, 8, logger) 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(terrain [][]Terrain, quality float64, logger Logger) *image.RGBA { + logger(LogArgs{}, "Creating thumbnail") srcWidth := len(terrain) srcHeight := len(terrain[0]) @@ -664,7 +717,7 @@ 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(data []byte, length int, logger Logger) { if length > len(data) { length = len(data) } @@ -673,7 +726,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(LogArgs{}, "Binary data (bits): %s", bits) } // createCombinedBinary combines the info JSON, map data, and mini-map data into a single binary buffer. @@ -785,3 +838,6 @@ type CombinedBinaryHeader struct { MiniMapOffset uint32 MiniMapSize uint32 } + +// Logger defines a function signature for logging with formatting. +type Logger func(LogArgs LogArgs, format string, v ...interface{}) From 997861a892c4894c7823a43dfa4733d14e8fabf0 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Sat, 27 Dec 2025 19:16:57 -0500 Subject: [PATCH 02/17] shuffle location of logger type and abstract logger creation fn --- map-generator/map_generator.go | 40 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index eed16ecffe..6912c30644 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -88,6 +88,27 @@ type LogArgs struct { isPerformance bool } +// Logger defines a function signature for logging with formatting and conditional output. +type Logger func(LogArgs LogArgs, format string, v ...interface{}) + +// createLogger creates a logger instance that handles formatting and conditional output +func createLogger(args GeneratorArgs) Logger { + // and handle checking against the performance and verbose flags + return func(LogArgs LogArgs, format string, v ...interface{}) { + if LogArgs.isVerbose && !args.Verbose { + return + } + if LogArgs.isPerformance && !args.Performance { + return + } + if args.Verbose { + log.Printf("[%s] %s", args.Name, fmt.Sprintf(format, v...)) + } else { + log.Printf(format, v...) + } + } +} + // GenerateMap is the main map-generator workflow. // - Maps each pixel to a Terrain type based on its blue value // - Removes small islands and lakes @@ -112,21 +133,7 @@ type LogArgs struct { // Misc Notes // - It normalizes map width/height to multiples of 4 for the mini map downscaling. func GenerateMap(args GeneratorArgs) (MapResult, error) { - // create a logger that will prepend the mapname in -verbose mode - // and handle checking against the performance and verbose flags - logger := func(LogArgs LogArgs, format string, v ...interface{}) { - if LogArgs.isVerbose && !args.Verbose { - return - } - if LogArgs.isPerformance && !args.Performance { - return - } - if args.Verbose { - log.Printf("[%s] %s", args.Name, fmt.Sprintf(format, v...)) - } else { - log.Printf(format, v...) - } - } + logger := createLogger(args) img, err := png.Decode(bytes.NewReader(args.ImageBuffer)) if err != nil { return MapResult{}, fmt.Errorf("failed to decode PNG: %w", err) @@ -838,6 +845,3 @@ type CombinedBinaryHeader struct { MiniMapOffset uint32 MiniMapSize uint32 } - -// Logger defines a function signature for logging with formatting. -type Logger func(LogArgs LogArgs, format string, v ...interface{}) From ff7744f3b762d601a00178e4751673b2f46915a6 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Sat, 27 Dec 2025 19:58:30 -0500 Subject: [PATCH 03/17] rename fn arg so it does not shadow type name --- map-generator/map_generator.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 6912c30644..7f11c38feb 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -89,16 +89,16 @@ type LogArgs struct { } // Logger defines a function signature for logging with formatting and conditional output. -type Logger func(LogArgs LogArgs, format string, v ...interface{}) +type Logger func(logArgs LogArgs, format string, v ...interface{}) // createLogger creates a logger instance that handles formatting and conditional output func createLogger(args GeneratorArgs) Logger { // and handle checking against the performance and verbose flags - return func(LogArgs LogArgs, format string, v ...interface{}) { - if LogArgs.isVerbose && !args.Verbose { + return func(logArgs LogArgs, format string, v ...interface{}) { + if logArgs.isVerbose && !args.Verbose { return } - if LogArgs.isPerformance && !args.Performance { + if logArgs.isPerformance && !args.Performance { return } if args.Verbose { From 05f931c1a868888ddef88549bdb4b709cbc3997e Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Sat, 27 Dec 2025 20:17:21 -0500 Subject: [PATCH 04/17] update nitpick comments in map generator logging --- map-generator/map_generator.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 7f11c38feb..8ff3e63de9 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -89,21 +89,25 @@ type LogArgs struct { } // Logger defines a function signature for logging with formatting and conditional output. +// Other than logArgs providing additional context, it is a proxy to log.Printf type Logger func(logArgs LogArgs, format string, v ...interface{}) // createLogger creates a logger instance that handles formatting and conditional output func createLogger(args GeneratorArgs) Logger { - // and handle checking against the performance and verbose flags return func(logArgs LogArgs, format string, v ...interface{}) { + // dont log verbose messages when not in verbose mode if logArgs.isVerbose && !args.Verbose { return } + // dont log performance messages when not in performance mode if logArgs.isPerformance && !args.Performance { return } + // log the name of the map first in verbose mode: [world] Log Message Here if args.Verbose { log.Printf("[%s] %s", args.Name, fmt.Sprintf(format, v...)) } else { + // by default, we proxy to log.Printf log.Printf(format, v...) } } @@ -146,12 +150,9 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { width = width - (width % 4) height = height - (height % 4) - // only perform this check if the performance flag is enabled - if args.Performance { - area := width * height - if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { - logger(LogArgs{isPerformance: true}, "⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize) - } + area := width * height + if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { + logger(LogArgs{isPerformance: true}, "⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize) } logger(LogArgs{}, "Processing Map: %s, dimensions: %dx%d", args.Name, width, height) From dd4b69c9a737567fd369d89037eb5c4604a94088 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Sat, 27 Dec 2025 20:29:13 -0500 Subject: [PATCH 05/17] unexport and rename LogArgs to loggerArgs type --- map-generator/map_generator.go | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 8ff3e63de9..a761a9fed5 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -82,19 +82,19 @@ type GeneratorArgs struct { Performance bool } -// LogArgs are the props that logs can define to control when they are shown -type LogArgs struct { +// loggerArgs are the props that logs can define to control when they are shown +type loggerArgs struct { isVerbose bool isPerformance bool } // Logger defines a function signature for logging with formatting and conditional output. -// Other than logArgs providing additional context, it is a proxy to log.Printf -type Logger func(logArgs LogArgs, format string, v ...interface{}) +// Other than loggerArgs providing additional context, it is a proxy to log.Printf +type Logger func(logArgs loggerArgs, format string, v ...interface{}) // createLogger creates a logger instance that handles formatting and conditional output func createLogger(args GeneratorArgs) Logger { - return func(logArgs LogArgs, format string, v ...interface{}) { + return func(logArgs loggerArgs, format string, v ...interface{}) { // dont log verbose messages when not in verbose mode if logArgs.isVerbose && !args.Verbose { return @@ -152,10 +152,10 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { area := width * height if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { - logger(LogArgs{isPerformance: true}, "⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize) + logger(loggerArgs{isPerformance: true}, "⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize) } - logger(LogArgs{}, "Processing Map: %s, dimensions: %dx%d", args.Name, width, height) + logger(loggerArgs{}, "Processing Map: %s, dimensions: %dx%d", args.Name, width, height) // Initialize terrain grid terrain := make([][]Terrain, width) @@ -208,15 +208,15 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { mapData4x, numLandTiles4x := packTerrain(terrain4x, logger) mapData16x, numLandTiles16x := packTerrain(terrain16x, logger) - logger(LogArgs{isVerbose: true}, "Land Tile Count (1x): %d", mapNumLandTiles) - logger(LogArgs{isVerbose: true}, "Land Tile Count (4x): %d", numLandTiles4x) - logger(LogArgs{isVerbose: true}, "Land Tile Count (16x): %d", numLandTiles16x) + logger(loggerArgs{isVerbose: true}, "Land Tile Count (1x): %d", mapNumLandTiles) + logger(loggerArgs{isVerbose: true}, "Land Tile Count (4x): %d", numLandTiles4x) + logger(loggerArgs{isVerbose: true}, "Land Tile Count (16x): %d", numLandTiles16x) if mapNumLandTiles == 0 { - logger(LogArgs{isPerformance: true}, "⚠️ Map has 0 land tiles") + logger(loggerArgs{isPerformance: true}, "⚠️ Map has 0 land tiles") } if mapNumLandTiles > maxRecommendedLandTileCount { - logger(LogArgs{isPerformance: true}, "⚠️ Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount) + logger(loggerArgs{isPerformance: true}, "⚠️ Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount) } return MapResult{ @@ -302,7 +302,7 @@ func createMiniMap(tm [][]Terrain) [][]Terrain { // shoreline if they neighbor Land. // Returns a list of coordinates for all shoreline Water tiles found. func processShore(terrain [][]Terrain, logger Logger) []Coord { - logger(LogArgs{}, "Identifying shorelines") + logger(loggerArgs{}, "Identifying shorelines") var shorelineWaters []Coord width := len(terrain) height := len(terrain[0]) @@ -340,7 +340,7 @@ func processShore(terrain [][]Terrain, logger Logger) []Coord { // 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, logger Logger) { - logger(LogArgs{}, "Setting Water tiles magnitude = Manhattan distance from nearest land") + logger(loggerArgs{}, "Setting Water tiles magnitude = Manhattan distance from nearest land") width := len(terrain) height := len(terrain[0]) @@ -422,7 +422,7 @@ func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord { // 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, logger Logger) { - logger(LogArgs{}, "Processing water bodies") + logger(loggerArgs{}, "Processing water bodies") visited := make(map[string]bool) type waterBody struct { @@ -467,14 +467,14 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger Logger) { for _, coord := range largestWaterBody.coords { terrain[coord.X][coord.Y].Ocean = true } - logger(LogArgs{}, "Identified ocean with %d water tiles", largestWaterBody.size) + logger(loggerArgs{}, "Identified ocean with %d water tiles", largestWaterBody.size) if removeSmall { // Remove small water bodies - logger(LogArgs{}, "Searching for small water bodies for removal") + logger(loggerArgs{}, "Searching for small water bodies for removal") for w := 1; w < len(waterBodies); w++ { if waterBodies[w].size < minLakeSize { - logger(LogArgs{isVerbose: true}, "Removing small lake at %d,%d (size %d)", waterBodies[w].coords[0].X, waterBodies[w].coords[0].Y, waterBodies[w].size) + logger(loggerArgs{isVerbose: true}, "Removing small lake at %d,%d (size %d)", waterBodies[w].coords[0].X, waterBodies[w].coords[0].Y, waterBodies[w].size) smallLakes++ for _, coord := range waterBodies[w].coords { terrain[coord.X][coord.Y].Type = Land @@ -482,7 +482,7 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger Logger) { } } } - logger(LogArgs{}, "Identified and removed %d bodies of water smaller than %d tiles", + logger(loggerArgs{}, "Identified and removed %d bodies of water smaller than %d tiles", smallLakes, minLakeSize) } @@ -490,7 +490,7 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger Logger) { shorelineWaters := processShore(terrain, logger) processDistToLand(shorelineWaters, terrain, logger) } else { - logger(LogArgs{}, "No water bodies found in the map") + logger(loggerArgs{}, "No water bodies found in the map") } } @@ -561,7 +561,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger Logger) { for _, body := range landBodies { if body.size < minIslandSize { - logger(LogArgs{isVerbose: true}, "Removing small island at %d,%d (size %d)", body.coords[0].X, body.coords[0].Y, body.size) + logger(loggerArgs{isVerbose: true}, "Removing small island at %d,%d (size %d)", body.coords[0].X, body.coords[0].Y, body.size) smallIslands++ for _, coord := range body.coords { terrain[coord.X][coord.Y].Type = Water @@ -570,7 +570,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger Logger) { } } - logger(LogArgs{}, "Identified and removed %d islands smaller than %d tiles", + logger(loggerArgs{}, "Identified and removed %d islands smaller than %d tiles", smallIslands, minIslandSize) } @@ -622,7 +622,7 @@ func packTerrain(terrain [][]Terrain, logger Logger) (data []byte, numLandTiles // 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, logger Logger) *image.RGBA { - logger(LogArgs{}, "Creating thumbnail") + logger(loggerArgs{}, "Creating thumbnail") srcWidth := len(terrain) srcHeight := len(terrain[0]) @@ -734,7 +734,7 @@ func logBinaryAsBits(data []byte, length int, logger Logger) { for i := 0; i < length; i++ { bits += fmt.Sprintf("%08b ", data[i]) } - logger(LogArgs{}, "Binary data (bits): %s", bits) + logger(loggerArgs{}, "Binary data (bits): %s", bits) } // createCombinedBinary combines the info JSON, map data, and mini-map data into a single binary buffer. From 7ee6a8d798270471bf128a2c1a82583f6e852b63 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Sun, 28 Dec 2025 23:40:33 -0500 Subject: [PATCH 06/17] working custom slog formater --- map-generator/logger.go | 101 +++++++++++++++++++++++++++++++++ map-generator/main.go | 20 ++++++- map-generator/map_generator.go | 97 ++++++++++--------------------- 3 files changed, 150 insertions(+), 68 deletions(-) create mode 100644 map-generator/logger.go diff --git a/map-generator/logger.go b/map-generator/logger.go new file mode 100644 index 0000000000..96d53d9b0b --- /dev/null +++ b/map-generator/logger.go @@ -0,0 +1,101 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "sync" +) + +var PerformanceLogTag = slog.String("tag", "performance") + +// PrettyHandler is a custom slog.Handler that outputs logs with each property on a separate line. +type PrettyHandler struct { + opts slog.HandlerOptions + w io.Writer + mu *sync.Mutex + attrs []slog.Attr + prefix string +} + +// NewPrettyHandler creates a new PrettyHandler. +func NewPrettyHandler(out io.Writer, opts *slog.HandlerOptions) *PrettyHandler { + h := &PrettyHandler{ + w: out, + mu: &sync.Mutex{}, + } + if opts != nil { + h.opts = *opts + } + if h.opts.Level == nil { + h.opts.Level = slog.LevelInfo + } + return h +} + +func (h *PrettyHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.opts.Level.Level() +} + +func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { + buf := &bytes.Buffer{} + + if r.Message != "" { + fmt.Fprintf(buf, "msg: %s\n", r.Message) + } + + currentAttrs := h.attrs + r.Attrs(func(a slog.Attr) bool { + currentAttrs = append(currentAttrs, a) + return true + }) + + for _, a := range currentAttrs { + h.appendAttr(buf, a, h.prefix) + } + + buf.WriteString("--\n") + + h.mu.Lock() + defer h.mu.Unlock() + _, err := h.w.Write(buf.Bytes()) + return err +} + +func (h *PrettyHandler) appendAttr(buf *bytes.Buffer, a slog.Attr, prefix string) { + key := a.Key + if prefix != "" { + key = prefix + "." + key + } + + if a.Value.Kind() == slog.KindGroup { + if key != "" { + prefix = key + } + for _, groupAttr := range a.Value.Group() { + h.appendAttr(buf, groupAttr, prefix) + } + } else if key != "" { + fmt.Fprintf(buf, "%s: %s\n", key, a.Value) + } +} + +func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + newHandler := *h + newHandler.attrs = append(newHandler.attrs, attrs...) + return &newHandler +} + +func (h *PrettyHandler) WithGroup(name string) slog.Handler { + if name == "" { + return h + } + newHandler := *h + if newHandler.prefix != "" { + newHandler.prefix += "." + } + newHandler.prefix += name + return &newHandler +} diff --git a/map-generator/main.go b/map-generator/main.go index f57737306e..5d6d45d9a1 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "log" + "log/slog" "os" "path/filepath" "strings" @@ -128,13 +129,15 @@ func processMap(name string, isTest bool) error { return fmt.Errorf("failed to parse info.json for %s: %w", name, err) } + var MapLogTag = slog.String("map", name) + logger := slog.Default().With(MapLogTag) + // Generate maps result, err := GenerateMap(GeneratorArgs{ ImageBuffer: imageBuffer, RemoveSmall: !isTest, // Don't remove small islands for test maps Name: name, - Verbose: verboseFlag, - Performance: performanceFlag && !isTest, + Logger: logger, }) if err != nil { return fmt.Errorf("failed to generate map for %s: %w", name, err) @@ -255,6 +258,19 @@ func main() { flag.BoolVar(&performanceFlag, "performance", false, "Adds additional logging checks for performance-based recommendations") flag.Parse() + var currentLevel = slog.LevelInfo + if verboseFlag { + currentLevel = slog.LevelDebug + } + + opts := &slog.HandlerOptions{ + Level: currentLevel, + } + // logger := slog.New(NewPrettyHandler(os.Stdout, opts)) + logger := slog.New(NewPrettyHandler(os.Stdout, opts)) + + slog.SetDefault(logger) + if err := loadTerrainMaps(); err != nil { log.Fatalf("Error generating terrain maps: %v", err) } diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index a761a9fed5..983e7e35d7 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -6,7 +6,7 @@ import ( "image" "image/color" "image/png" - "log" + "log/slog" "math" "github.com/chai2010/webp" @@ -22,11 +22,10 @@ const ( const ( minRecommendedPixelSize = 2000000 maxRecommendedPixelSize = 3000000 + // the recommended max number of land tiles in the output bin at full size + maxRecommendedLandTileCount = 3000000 ) -// the recommended max number of land tiles in the output bin at full size -const maxRecommendedLandTileCount = 3000000 - // Holds raw RGBA image data for the thumbnail type ThumbData struct { Data []byte @@ -78,39 +77,7 @@ type GeneratorArgs struct { Name string ImageBuffer []byte RemoveSmall bool - Verbose bool - Performance bool -} - -// loggerArgs are the props that logs can define to control when they are shown -type loggerArgs struct { - isVerbose bool - isPerformance bool -} - -// Logger defines a function signature for logging with formatting and conditional output. -// Other than loggerArgs providing additional context, it is a proxy to log.Printf -type Logger func(logArgs loggerArgs, format string, v ...interface{}) - -// createLogger creates a logger instance that handles formatting and conditional output -func createLogger(args GeneratorArgs) Logger { - return func(logArgs loggerArgs, format string, v ...interface{}) { - // dont log verbose messages when not in verbose mode - if logArgs.isVerbose && !args.Verbose { - return - } - // dont log performance messages when not in performance mode - if logArgs.isPerformance && !args.Performance { - return - } - // log the name of the map first in verbose mode: [world] Log Message Here - if args.Verbose { - log.Printf("[%s] %s", args.Name, fmt.Sprintf(format, v...)) - } else { - // by default, we proxy to log.Printf - log.Printf(format, v...) - } - } + Logger *slog.Logger } // GenerateMap is the main map-generator workflow. @@ -137,7 +104,7 @@ func createLogger(args GeneratorArgs) Logger { // Misc Notes // - It normalizes map width/height to multiples of 4 for the mini map downscaling. func GenerateMap(args GeneratorArgs) (MapResult, error) { - logger := createLogger(args) + logger := args.Logger img, err := png.Decode(bytes.NewReader(args.ImageBuffer)) if err != nil { return MapResult{}, fmt.Errorf("failed to decode PNG: %w", err) @@ -152,10 +119,10 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { area := width * height if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { - logger(loggerArgs{isPerformance: true}, "⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize) + logger.Debug(fmt.Sprintf("⚠️ Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize), PerformanceLogTag) } - logger(loggerArgs{}, "Processing Map: %s, dimensions: %dx%d", args.Name, width, height) + logger.Info(fmt.Sprintf("Processing Map: %s, dimensions: %dx%d", args.Name, width, height)) // Initialize terrain grid terrain := make([][]Terrain, width) @@ -208,15 +175,15 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { mapData4x, numLandTiles4x := packTerrain(terrain4x, logger) mapData16x, numLandTiles16x := packTerrain(terrain16x, logger) - logger(loggerArgs{isVerbose: true}, "Land Tile Count (1x): %d", mapNumLandTiles) - logger(loggerArgs{isVerbose: true}, "Land Tile Count (4x): %d", numLandTiles4x) - logger(loggerArgs{isVerbose: true}, "Land Tile Count (16x): %d", numLandTiles16x) + 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 { - logger(loggerArgs{isPerformance: true}, "⚠️ Map has 0 land tiles") + logger.Debug("⚠️ Map has 0 land tiles", PerformanceLogTag) } if mapNumLandTiles > maxRecommendedLandTileCount { - logger(loggerArgs{isPerformance: true}, "⚠️ Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount) + logger.Debug(fmt.Sprintf("⚠️ Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) } return MapResult{ @@ -301,8 +268,8 @@ 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, logger Logger) []Coord { - logger(loggerArgs{}, "Identifying shorelines") +func processShore(terrain [][]Terrain, logger *slog.Logger) []Coord { + logger.Info("Identifying shorelines") var shorelineWaters []Coord width := len(terrain) height := len(terrain[0]) @@ -339,8 +306,8 @@ func processShore(terrain [][]Terrain, logger Logger) []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, logger Logger) { - logger(loggerArgs{}, "Setting Water tiles magnitude = Manhattan distance from nearest land") +func processDistToLand(shorelineWaters []Coord, terrain [][]Terrain, logger *slog.Logger) { + logger.Info("Setting Water tiles magnitude = Manhattan distance from nearest land") width := len(terrain) height := len(terrain[0]) @@ -421,8 +388,8 @@ 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, logger Logger) { - logger(loggerArgs{}, "Processing water bodies") +func processWater(terrain [][]Terrain, removeSmall bool, logger *slog.Logger) { + logger.Info("Processing water bodies") visited := make(map[string]bool) type waterBody struct { @@ -467,14 +434,14 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger Logger) { for _, coord := range largestWaterBody.coords { terrain[coord.X][coord.Y].Ocean = true } - logger(loggerArgs{}, "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 - logger(loggerArgs{}, "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(loggerArgs{isVerbose: true}, "Removing small lake at %d,%d (size %d)", waterBodies[w].coords[0].X, waterBodies[w].coords[0].Y, waterBodies[w].size) + 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)) smallLakes++ for _, coord := range waterBodies[w].coords { terrain[coord.X][coord.Y].Type = Land @@ -482,15 +449,14 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger Logger) { } } } - logger(loggerArgs{}, "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, logger) processDistToLand(shorelineWaters, terrain, logger) } else { - logger(loggerArgs{}, "No water bodies found in the map") + logger.Info("No water bodies found in the map") } } @@ -525,7 +491,7 @@ 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, logger Logger) { +func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger *slog.Logger) { if !removeSmall { return } @@ -561,7 +527,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger Logger) { for _, body := range landBodies { if body.size < minIslandSize { - logger(loggerArgs{isVerbose: true}, "Removing small island at %d,%d (size %d)", body.coords[0].X, body.coords[0].Y, body.size) + logger.Debug(fmt.Sprintf("Removing small island at %d,%d (size %d)", body.coords[0].X, body.coords[0].Y, body.size)) smallIslands++ for _, coord := range body.coords { terrain[coord.X][coord.Y].Type = Water @@ -570,8 +536,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger Logger) { } } - logger(loggerArgs{}, "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. @@ -582,7 +547,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger Logger) { // - 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, logger Logger) (data []byte, numLandTiles int) { +func packTerrain(terrain [][]Terrain, logger *slog.Logger) (data []byte, numLandTiles int) { width := len(terrain) height := len(terrain[0]) packedData := make([]byte, width*height) @@ -621,8 +586,8 @@ func packTerrain(terrain [][]Terrain, logger Logger) (data []byte, 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, logger Logger) *image.RGBA { - logger(loggerArgs{}, "Creating thumbnail") +func createMapThumbnail(terrain [][]Terrain, quality float64, logger *slog.Logger) *image.RGBA { + logger.Info("Creating thumbnail") srcWidth := len(terrain) srcHeight := len(terrain[0]) @@ -725,7 +690,7 @@ 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, logger Logger) { +func logBinaryAsBits(data []byte, length int, logger *slog.Logger) { if length > len(data) { length = len(data) } @@ -734,7 +699,7 @@ func logBinaryAsBits(data []byte, length int, logger Logger) { for i := 0; i < length; i++ { bits += fmt.Sprintf("%08b ", data[i]) } - logger(loggerArgs{}, "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. From 315de9ea541a49e0e95d1c007a5f828fa8de7529 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 01:36:08 -0500 Subject: [PATCH 07/17] abstract to slog logger and clean up log level logic --- map-generator/logger.go | 115 ++++++++++++++++++++++----------- map-generator/main.go | 51 ++++++++++++--- map-generator/map_generator.go | 10 +-- 3 files changed, 124 insertions(+), 52 deletions(-) diff --git a/map-generator/logger.go b/map-generator/logger.go index 96d53d9b0b..6d6122bfa5 100644 --- a/map-generator/logger.go +++ b/map-generator/logger.go @@ -6,25 +6,45 @@ import ( "fmt" "io" "log/slog" + "strings" "sync" ) +type LogFlags struct { + performance bool + removal bool +} + +// LevelAll allows for manually setting a log level that outputs all messages, regardless of other passed flags +const LevelAll = slog.Level(-8) + +// PerformanceLogTag is a slog attribute used to tag performance-related log messages. var PerformanceLogTag = slog.String("tag", "performance") -// PrettyHandler is a custom slog.Handler that outputs logs with each property on a separate line. -type PrettyHandler struct { +// RemovalLogTag is a slog attribute used to tag land/water removal-related log messages. +var RemovalLogTag = slog.String("tag", "removal") + +// GeneratorLogger is a custom slog.Handler that outputs logs based on verbosity and performance flags. +type GeneratorLogger struct { opts slog.HandlerOptions w io.Writer mu *sync.Mutex attrs []slog.Attr prefix string + flags LogFlags } -// NewPrettyHandler creates a new PrettyHandler. -func NewPrettyHandler(out io.Writer, opts *slog.HandlerOptions) *PrettyHandler { - h := &PrettyHandler{ - w: out, - mu: &sync.Mutex{}, +// NewGeneratorLogger creates a new GeneratorLogger. +// It initializes a handler with specific output, options, and flags for verbosity and performance. +func NewGeneratorLogger( + out io.Writer, + opts *slog.HandlerOptions, + flags LogFlags) *GeneratorLogger { + + h := &GeneratorLogger{ + w: out, + mu: &sync.Mutex{}, + flags: flags, } if opts != nil { h.opts = *opts @@ -35,60 +55,81 @@ func NewPrettyHandler(out io.Writer, opts *slog.HandlerOptions) *PrettyHandler { return h } -func (h *PrettyHandler) Enabled(_ context.Context, level slog.Level) bool { +// Enabled checks if a given log level is enabled for this handler. +func (h *GeneratorLogger) Enabled(_ context.Context, level slog.Level) bool { return level >= h.opts.Level.Level() } -func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { - buf := &bytes.Buffer{} +// Handle processes a log record. +// It formats the log message and decides whether to output it based on log level and flags +func (h *GeneratorLogger) Handle(_ context.Context, r slog.Record) error { + isPerformanceLog := false + isRemovalLog := false + var mapName string - if r.Message != "" { - fmt.Fprintf(buf, "msg: %s\n", r.Message) + findAttrs := func(a slog.Attr) { + if a.Equal(PerformanceLogTag) { + isPerformanceLog = true + } + if a.Equal(RemovalLogTag) { + isRemovalLog = true + } + if a.Key == "map" { + mapName = a.Value.String() + } } - currentAttrs := h.attrs + // Check record attributes for performance tag and map name r.Attrs(func(a slog.Attr) bool { - currentAttrs = append(currentAttrs, a) + findAttrs(a) return true }) - for _, a := range currentAttrs { - h.appendAttr(buf, a, h.prefix) + // Check handler's own attributes for performance tag and map name + for _, a := range h.attrs { + findAttrs(a) } - buf.WriteString("--\n") + // Don't log messages if the flags are not set + // If the log level is set to LevelAll, disregard + if h.opts.Level != LevelAll && isPerformanceLog && !h.flags.performance { + return nil + } + if h.opts.Level != LevelAll && (isRemovalLog && !h.flags.removal) { + return nil + } - h.mu.Lock() - defer h.mu.Unlock() - _, err := h.w.Write(buf.Bytes()) - return err -} + buf := &bytes.Buffer{} -func (h *PrettyHandler) appendAttr(buf *bytes.Buffer, a slog.Attr, prefix string) { - key := a.Key - if prefix != "" { - key = prefix + "." + key + // Add map name as a prefix in log Level DEBUG and ALL + if h.opts.Level == slog.LevelDebug || h.opts.Level == LevelAll && mapName != "" { + mapName = strings.Trim(mapName, `"`) + fmt.Fprintf(buf, "[%s] ", mapName) } - if a.Value.Kind() == slog.KindGroup { - if key != "" { - prefix = key - } - for _, groupAttr := range a.Value.Group() { - h.appendAttr(buf, groupAttr, prefix) - } - } else if key != "" { - fmt.Fprintf(buf, "%s: %s\n", key, a.Value) + // Add prefix for performance messages + if isPerformanceLog { + fmt.Fprintf(buf, "[PERF] ") } + + fmt.Fprintln(buf, r.Message) + + h.mu.Lock() + defer h.mu.Unlock() + _, err := h.w.Write(buf.Bytes()) + return err } -func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { +// WithAttrs returns a new handler with the given attributes added. +func (h *GeneratorLogger) WithAttrs(attrs []slog.Attr) slog.Handler { newHandler := *h newHandler.attrs = append(newHandler.attrs, attrs...) return &newHandler } -func (h *PrettyHandler) WithGroup(name string) slog.Handler { +// WithGroup returns a new handler with the given group name. +// The group name is added as a prefix to subsequent log messages. +func (h *GeneratorLogger) WithGroup(name string) slog.Handler { if name == "" { return h } diff --git a/map-generator/main.go b/map-generator/main.go index 5d6d45d9a1..19169e432c 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -14,8 +14,12 @@ import ( // mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument. var mapsFlag string + +// verboseFlag performanceFlag and removalFlag logLevelFlag impact logging, see ./logger.go var verboseFlag bool var performanceFlag bool +var removalFlag bool +var logLevelFlag string // maps defines the registry of available maps to be processed. // Each entry contains the folder name and a flag indicating if it's a test map. @@ -253,21 +257,48 @@ func loadTerrainMaps() error { // It parses flags and triggers the map generation process. func main() { flag.StringVar(&mapsFlag, "maps", "", "optional comma-separated list of maps to process. ex: --maps=world,eastasia,big_plains") - flag.BoolVar(&verboseFlag, "verbose", false, "Turns on additional logging during the map generation process") - flag.BoolVar(&verboseFlag, "v", false, "Turns on additional logging (shorthand)") - flag.BoolVar(&performanceFlag, "performance", false, "Adds additional logging checks for performance-based recommendations") + flag.StringVar(&logLevelFlag, "log-level", "", "Explicitly sets the log level to one of: ALL, DEBUG, INFO (default), WARN, ERROR.") + flag.BoolVar(&verboseFlag, "verbose", false, "Adds additional logging and prefixes logs with the [mapname]. Alias of log-level=DEBUG.") + flag.BoolVar(&verboseFlag, "v", false, "-verbose shorthand") + flag.BoolVar(&performanceFlag, "log-performance", false, "Adds additional logging for performance-based recommendations, sets log-level=DEBUG") + flag.BoolVar(&removalFlag, "log-removal", false, "Adds additional logging of removed island and lake position/size, sets log-level=DEBUG") flag.Parse() - var currentLevel = slog.LevelInfo - if verboseFlag { - currentLevel = slog.LevelDebug + var level = slog.LevelInfo + + if verboseFlag || performanceFlag || removalFlag { + level = slog.LevelDebug } - opts := &slog.HandlerOptions{ - Level: currentLevel, + // parse the log-level input string to the slog.Level type + if logLevelFlag != "" { + switch strings.ToLower(logLevelFlag) { + case "all": + level = LevelAll + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + fmt.Printf("invalid log level: %s, defaulting to info\n", logLevelFlag) + level = slog.LevelInfo + } } - // logger := slog.New(NewPrettyHandler(os.Stdout, opts)) - logger := slog.New(NewPrettyHandler(os.Stdout, opts)) + + logger := slog.New(NewGeneratorLogger( + os.Stdout, + &slog.HandlerOptions{ + Level: level, + }, + LogFlags{ + performance: performanceFlag, + removal: removalFlag, + }, + )) slog.SetDefault(logger) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 983e7e35d7..9d65951a42 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -119,7 +119,7 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { 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) + logger.Info(fmt.Sprintf("Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize), PerformanceLogTag) } logger.Info(fmt.Sprintf("Processing Map: %s, dimensions: %dx%d", args.Name, width, height)) @@ -180,10 +180,10 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { logger.Debug(fmt.Sprintf("Land Tile Count (16x): %d", numLandTiles16x)) if mapNumLandTiles == 0 { - logger.Debug("⚠️ Map has 0 land tiles", PerformanceLogTag) + logger.Info("Map has 0 land tiles", PerformanceLogTag) } if mapNumLandTiles > maxRecommendedLandTileCount { - logger.Debug(fmt.Sprintf("⚠️ Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) + logger.Info(fmt.Sprintf("Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) } return MapResult{ @@ -441,7 +441,7 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger *slog.Logger) { 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)) + 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 @@ -527,7 +527,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger *slog.Logg 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)) + 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 From 798fffcfce6631b52bfb3d0b8a22fa2f376615d5 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 02:12:09 -0500 Subject: [PATCH 08/17] clean up readme and comments --- map-generator/README.md | 21 ++++++++++++++++++--- map-generator/logger.go | 4 ++-- map-generator/main.go | 32 +++++++++++++++++--------------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/map-generator/README.md b/map-generator/README.md index cebeca9192..281bd96aa3 100644 --- a/map-generator/README.md +++ b/map-generator/README.md @@ -43,9 +43,24 @@ To process a subset of maps, pass a comma-separated list: ## Command Line Flags -- `--maps`: Comma-separated list of maps to process. -- `--verbose` or `-v`: Turns on additional logging during the map generation process. -- `--performance`: Adds additional logging checks for performance-based recommendations. +- `--maps`: Optional comma-separated list of maps to process. + - ex: `go run . --maps=world,eastasia,big_plains` + +### Logging + +- `--log-level`: Explicitly sets the log level. + - ex: `go run . --log-level=debug` + - values: `ALL`, `DEBUG`, `INFO` (default), `WARN`, `ERROR`. +- `--verbose` or `-v`: Adds additional logging and prefixes logs with the `[mapname]`. Alias of `--log-level=DEBUG`. +- `--debug-performance`: Adds additional logging for performance-based recommendations, sets `--log-level=DEBUG`. +- `--debug-removal`: Adds additional logging of removed island and lake position/size, sets `--log-level=DEBUG`. + +The Generator outputs logs using `slog` with standard log-levels, and an additional ALL level. + +The `--verbose`, `-v`, `--debug-performance`, and `--debug-removal` flags all set the log level to `DEBUG`. +`debug-performance` and `debug-removal` are opt-in on top of the debug log level, as they can produce wordy output. You must pass the specific flag to see the corresponding logs if the `log-level` is set to `DEBUG`. + +Setting `--log-level=ALL` will output all possible logs, including all `DEBUG` tiers, regardless of whether the specific flags are passed. ## Create image.png diff --git a/map-generator/logger.go b/map-generator/logger.go index 6d6122bfa5..e93c261ec6 100644 --- a/map-generator/logger.go +++ b/map-generator/logger.go @@ -24,7 +24,7 @@ var PerformanceLogTag = slog.String("tag", "performance") // RemovalLogTag is a slog attribute used to tag land/water removal-related log messages. var RemovalLogTag = slog.String("tag", "removal") -// GeneratorLogger is a custom slog.Handler that outputs logs based on verbosity and performance flags. +// GeneratorLogger is a custom slog.Handler that outputs logs based on log level and performance flags. type GeneratorLogger struct { opts slog.HandlerOptions w io.Writer @@ -35,7 +35,7 @@ type GeneratorLogger struct { } // NewGeneratorLogger creates a new GeneratorLogger. -// It initializes a handler with specific output, options, and flags for verbosity and performance. +// It initializes a handler with specific output, options, and flags for log level and performance. func NewGeneratorLogger( out io.Writer, opts *slog.HandlerOptions, diff --git a/map-generator/main.go b/map-generator/main.go index 19169e432c..4c43e9c709 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -12,15 +12,6 @@ import ( "sync" ) -// mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument. -var mapsFlag string - -// verboseFlag performanceFlag and removalFlag logLevelFlag impact logging, see ./logger.go -var verboseFlag bool -var performanceFlag bool -var removalFlag bool -var logLevelFlag string - // maps defines the registry of available maps to be processed. // Each entry contains the folder name and a flag indicating if it's a test map. // @@ -74,6 +65,16 @@ var maps = []struct { {Name: "giantworldmap", IsTest: true}, } +// mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument. +var mapsFlag string + +// The log-level (most -> least wordy): ALL, DEBUG, INFO (default), WARN, ERROR +var logLevelFlag string + +var verboseFlag bool // sets log-level=DEBUG +var debugPerformanceFlag bool // opts-in to performance checks and sets log-level=DEBUG +var debugRemovalFlag bool // opts-in to island/lake removal logging and sets log-level=DEBUG + // outputMapDir returns the absolute path to the directory where generated map files should be written. // It distinguishes between test and production output locations. func outputMapDir(isTest bool) (string, error) { @@ -102,7 +103,8 @@ func inputMapDir(isTest bool) (string, error) { } // processMap handles the end-to-end generation for a single map. -// It reads the source image and JSON, generates the terrain data, and writes the binary outputs and updated manifest. +// It reads the source image and JSON, generates the terrain data, and writes +// the binary outputs and updated manifest. func processMap(name string, isTest bool) error { outputMapBaseDir, err := outputMapDir(isTest) if err != nil { @@ -260,13 +262,13 @@ func main() { flag.StringVar(&logLevelFlag, "log-level", "", "Explicitly sets the log level to one of: ALL, DEBUG, INFO (default), WARN, ERROR.") flag.BoolVar(&verboseFlag, "verbose", false, "Adds additional logging and prefixes logs with the [mapname]. Alias of log-level=DEBUG.") flag.BoolVar(&verboseFlag, "v", false, "-verbose shorthand") - flag.BoolVar(&performanceFlag, "log-performance", false, "Adds additional logging for performance-based recommendations, sets log-level=DEBUG") - flag.BoolVar(&removalFlag, "log-removal", false, "Adds additional logging of removed island and lake position/size, sets log-level=DEBUG") + flag.BoolVar(&debugPerformanceFlag, "log-performance", false, "Adds additional logging for performance-based recommendations, sets log-level=DEBUG") + flag.BoolVar(&debugRemovalFlag, "log-removal", false, "Adds additional logging of removed island and lake position/size, sets log-level=DEBUG") flag.Parse() var level = slog.LevelInfo - if verboseFlag || performanceFlag || removalFlag { + if verboseFlag || debugPerformanceFlag || debugRemovalFlag { level = slog.LevelDebug } @@ -295,8 +297,8 @@ func main() { Level: level, }, LogFlags{ - performance: performanceFlag, - removal: removalFlag, + performance: debugPerformanceFlag, + removal: debugRemovalFlag, }, )) From da4c70b75bf84012640fd133eff7cb589d7208be Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 02:30:30 -0500 Subject: [PATCH 09/17] update docs --- map-generator/main.go | 3 +-- map-generator/map_generator.go | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/map-generator/main.go b/map-generator/main.go index 4c43e9c709..9220289adf 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -103,8 +103,7 @@ func inputMapDir(isTest bool) (string, error) { } // processMap handles the end-to-end generation for a single map. -// It reads the source image and JSON, generates the terrain data, and writes -// the binary outputs and updated manifest. +// It reads the source image and JSON, generates the terrain data, and writes the binary outputs and updated manifest. func processMap(name string, isTest bool) error { outputMapBaseDir, err := outputMapDir(isTest) if err != nil { diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 9d65951a42..12137eb71f 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -18,8 +18,8 @@ const ( minLakeSize = 200 ) -// the recommended max area pixel size for input images const ( + // 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 @@ -117,13 +117,13 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { width = width - (width % 4) height = height - (height % 4) + logger.Info(fmt.Sprintf("Processing Map: %s, dimensions: %dx%d", args.Name, width, height)) + area := width * height if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { logger.Info(fmt.Sprintf("Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize), PerformanceLogTag) } - logger.Info(fmt.Sprintf("Processing Map: %s, dimensions: %dx%d", args.Name, width, height)) - // Initialize terrain grid terrain := make([][]Terrain, width) for x := range terrain { @@ -180,7 +180,7 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { logger.Debug(fmt.Sprintf("Land Tile Count (16x): %d", numLandTiles16x)) if mapNumLandTiles == 0 { - logger.Info("Map has 0 land tiles", PerformanceLogTag) + logger.Error("Map has 0 land tiles") } if mapNumLandTiles > maxRecommendedLandTileCount { logger.Info(fmt.Sprintf("Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) From ecba038040877dd52be95f1f63a571fab79d482d Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 03:00:29 -0500 Subject: [PATCH 10/17] switch to context-based passing for logger --- map-generator/logger.go | 21 ++++++++++++++- map-generator/main.go | 14 +++++----- map-generator/map_generator.go | 49 +++++++++++++++++++--------------- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/map-generator/logger.go b/map-generator/logger.go index e93c261ec6..6b55e49f85 100644 --- a/map-generator/logger.go +++ b/map-generator/logger.go @@ -1,3 +1,5 @@ +// This is the custom logger providing the multi-level and flag-based logging for +// the map-generator. It uses slog. package main import ( @@ -10,6 +12,7 @@ import ( "sync" ) +// Flags supported for conditional logging type LogFlags struct { performance bool removal bool @@ -35,7 +38,7 @@ type GeneratorLogger struct { } // NewGeneratorLogger creates a new GeneratorLogger. -// It initializes a handler with specific output, options, and flags for log level and performance. +// It initializes a handler with specific output, options, and flags func NewGeneratorLogger( out io.Writer, opts *slog.HandlerOptions, @@ -140,3 +143,19 @@ func (h *GeneratorLogger) WithGroup(name string) slog.Handler { newHandler.prefix += name return &newHandler } + +type loggerKey struct{} + +// LoggerFromContext retrieves the logger from the context. +// If no logger is found, it returns the default logger. +func LoggerFromContext(ctx context.Context) *slog.Logger { + if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { + return logger + } + return slog.Default() +} + +// ContextWithLogger returns a new context with the provided logger. +func ContextWithLogger(ctx context.Context, logger *slog.Logger) context.Context { + return context.WithValue(ctx, loggerKey{}, logger) +} diff --git a/map-generator/main.go b/map-generator/main.go index 9220289adf..548eed91e2 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "flag" "fmt" @@ -104,7 +105,7 @@ func inputMapDir(isTest bool) (string, error) { // processMap handles the end-to-end generation for a single map. // It reads the source image and JSON, generates the terrain data, and writes the binary outputs and updated manifest. -func processMap(name string, isTest bool) error { +func processMap(ctx context.Context, name string, isTest bool) error { outputMapBaseDir, err := outputMapDir(isTest) if err != nil { return fmt.Errorf("failed to get map directory: %w", err) @@ -134,15 +135,11 @@ func processMap(name string, isTest bool) error { return fmt.Errorf("failed to parse info.json for %s: %w", name, err) } - var MapLogTag = slog.String("map", name) - logger := slog.Default().With(MapLogTag) - // Generate maps - result, err := GenerateMap(GeneratorArgs{ + result, err := GenerateMap(ctx, GeneratorArgs{ ImageBuffer: imageBuffer, RemoveSmall: !isTest, // Don't remove small islands for test maps Name: name, - Logger: logger, }) if err != nil { return fmt.Errorf("failed to generate map for %s: %w", name, err) @@ -234,7 +231,10 @@ func loadTerrainMaps() error { mapItem := mapItem go func() { defer wg.Done() - if err := processMap(mapItem.Name, mapItem.IsTest); err != nil { + var MapLogTag = slog.String("map", mapItem.Name) + logger := slog.Default().With(MapLogTag) + ctx := ContextWithLogger(context.Background(), logger) + if err := processMap(ctx, mapItem.Name, mapItem.IsTest); err != nil { errChan <- err } }() diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 12137eb71f..a47e7fc2ab 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -2,11 +2,11 @@ package main import ( "bytes" + "context" "fmt" "image" "image/color" "image/png" - "log/slog" "math" "github.com/chai2010/webp" @@ -77,7 +77,6 @@ type GeneratorArgs struct { Name string ImageBuffer []byte RemoveSmall bool - Logger *slog.Logger } // GenerateMap is the main map-generator workflow. @@ -103,8 +102,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) { - logger := args.Logger +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) @@ -152,16 +151,16 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { } } - removeSmallIslands(terrain, args.RemoveSmall, logger) - processWater(terrain, args.RemoveSmall, logger) + removeSmallIslands(ctx, terrain, args.RemoveSmall) + processWater(ctx, terrain, args.RemoveSmall) terrain4x := createMiniMap(terrain) - processWater(terrain4x, false, logger) + processWater(ctx, terrain4x, false) terrain16x := createMiniMap(terrain4x) - processWater(terrain16x, false, logger) + processWater(ctx, terrain16x, false) - thumb := createMapThumbnail(terrain4x, 0.5, logger) + thumb := createMapThumbnail(ctx, terrain4x, 0.5) webp, err := convertToWebP(ThumbData{ Data: thumb.Pix, Width: thumb.Bounds().Dx(), @@ -171,9 +170,9 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) { return MapResult{}, fmt.Errorf("failed to save thumbnail: %w", err) } - mapData, mapNumLandTiles := packTerrain(terrain, logger) - mapData4x, numLandTiles4x := packTerrain(terrain4x, logger) - mapData16x, numLandTiles16x := packTerrain(terrain16x, logger) + 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)) @@ -268,7 +267,8 @@ 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, logger *slog.Logger) []Coord { +func processShore(ctx context.Context, terrain [][]Terrain) []Coord { + logger := LoggerFromContext(ctx) logger.Info("Identifying shorelines") var shorelineWaters []Coord width := len(terrain) @@ -306,7 +306,8 @@ func processShore(terrain [][]Terrain, logger *slog.Logger) []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, logger *slog.Logger) { +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) @@ -388,7 +389,8 @@ 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, logger *slog.Logger) { +func processWater(ctx context.Context, terrain [][]Terrain, removeSmall bool) { + logger := LoggerFromContext(ctx) logger.Info("Processing water bodies") visited := make(map[string]bool) @@ -453,8 +455,8 @@ func processWater(terrain [][]Terrain, removeSmall bool, logger *slog.Logger) { } // Process shorelines and distances - shorelineWaters := processShore(terrain, logger) - processDistToLand(shorelineWaters, terrain, logger) + shorelineWaters := processShore(ctx, terrain) + processDistToLand(ctx, shorelineWaters, terrain) } else { logger.Info("No water bodies found in the map") } @@ -491,7 +493,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, logger *slog.Logger) { +func removeSmallIslands(ctx context.Context, terrain [][]Terrain, removeSmall bool) { + logger := LoggerFromContext(ctx) if !removeSmall { return } @@ -547,7 +550,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool, logger *slog.Logg // - 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, logger *slog.Logger) (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) @@ -579,14 +582,15 @@ func packTerrain(terrain [][]Terrain, logger *slog.Logger) (data []byte, numLand } } - logBinaryAsBits(packedData, 8, logger) + 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, logger *slog.Logger) *image.RGBA { +func createMapThumbnail(ctx context.Context, terrain [][]Terrain, quality float64) *image.RGBA { + logger := LoggerFromContext(ctx) logger.Info("Creating thumbnail") srcWidth := len(terrain) @@ -690,7 +694,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, logger *slog.Logger) { +func logBinaryAsBits(ctx context.Context, data []byte, length int) { + logger := LoggerFromContext(ctx) if length > len(data) { length = len(data) } From 60a193dd6613469dbbce0e3750f95387afc38a50 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 03:36:09 -0500 Subject: [PATCH 11/17] further isolate logger related logic --- map-generator/logger.go | 48 ++++++++++++++++++++++++++++++++++----- map-generator/main.go | 50 ++++++++--------------------------------- 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/map-generator/logger.go b/map-generator/logger.go index 6b55e49f85..fb2544c420 100644 --- a/map-generator/logger.go +++ b/map-generator/logger.go @@ -12,13 +12,14 @@ import ( "sync" ) -// Flags supported for conditional logging type LogFlags struct { - performance bool - removal bool + logLevel string // The log-level (most -> least wordy): ALL, DEBUG, INFO (default), WARN, ERROR + verbose bool // sets log-level=DEBUG + performance bool // opts-in to performance checks and sets log-level=DEBUG + removal bool // opts-in to island/lake removal logging and sets log-level=DEBUG } -// LevelAll allows for manually setting a log level that outputs all messages, regardless of other passed flags +// LevelAll is a custom log Level that outputs all messages, regardless of other passed flags const LevelAll = slog.Level(-8) // PerformanceLogTag is a slog attribute used to tag performance-related log messages. @@ -27,6 +28,42 @@ var PerformanceLogTag = slog.String("tag", "performance") // RemovalLogTag is a slog attribute used to tag land/water removal-related log messages. var RemovalLogTag = slog.String("tag", "removal") +// DetermineLogLevel determines the log level based on the LogFlags +// It prioritizes the log level flag over the default, and switches to debug if performance or removal flags are set. +func DetermineLogLevel( + logFlags LogFlags) slog.Level { + + var level = slog.LevelInfo + if logFlags.verbose { + level = slog.LevelDebug + } + + // switch to debug if any of the optional flags is enabled + if logFlags.performance || logFlags.removal { + level = slog.LevelDebug + } + + // parse the log-level input string to the slog.Level type + if logFlags.logLevel != "" { + switch strings.ToLower(logFlags.logLevel) { + case "all": + level = LevelAll + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + fmt.Printf("invalid log level: %s, defaulting to info\n", logFlags.logLevel) + level = slog.LevelInfo + } + } + return level +} + // GeneratorLogger is a custom slog.Handler that outputs logs based on log level and performance flags. type GeneratorLogger struct { opts slog.HandlerOptions @@ -64,7 +101,8 @@ func (h *GeneratorLogger) Enabled(_ context.Context, level slog.Level) bool { } // Handle processes a log record. -// It formats the log message and decides whether to output it based on log level and flags +// It decides whether to output each record based on log level and flags +// On output, it formats the log message with any extra formatting func (h *GeneratorLogger) Handle(_ context.Context, r slog.Record) error { isPerformanceLog := false isRemovalLog := false diff --git a/map-generator/main.go b/map-generator/main.go index 548eed91e2..bdb1f7de30 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -69,12 +69,8 @@ var maps = []struct { // mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument. var mapsFlag string -// The log-level (most -> least wordy): ALL, DEBUG, INFO (default), WARN, ERROR -var logLevelFlag string - -var verboseFlag bool // sets log-level=DEBUG -var debugPerformanceFlag bool // opts-in to performance checks and sets log-level=DEBUG -var debugRemovalFlag bool // opts-in to island/lake removal logging and sets log-level=DEBUG +// logFlags holds all the flags related to configuring the map-generator logging +var logFlags LogFlags // outputMapDir returns the absolute path to the directory where generated map files should be written. // It distinguishes between test and production output locations. @@ -258,47 +254,19 @@ func loadTerrainMaps() error { // It parses flags and triggers the map generation process. func main() { flag.StringVar(&mapsFlag, "maps", "", "optional comma-separated list of maps to process. ex: --maps=world,eastasia,big_plains") - flag.StringVar(&logLevelFlag, "log-level", "", "Explicitly sets the log level to one of: ALL, DEBUG, INFO (default), WARN, ERROR.") - flag.BoolVar(&verboseFlag, "verbose", false, "Adds additional logging and prefixes logs with the [mapname]. Alias of log-level=DEBUG.") - flag.BoolVar(&verboseFlag, "v", false, "-verbose shorthand") - flag.BoolVar(&debugPerformanceFlag, "log-performance", false, "Adds additional logging for performance-based recommendations, sets log-level=DEBUG") - flag.BoolVar(&debugRemovalFlag, "log-removal", false, "Adds additional logging of removed island and lake position/size, sets log-level=DEBUG") + flag.StringVar(&logFlags.logLevel, "log-level", "", "Explicitly sets the log level to one of: ALL, DEBUG, INFO (default), WARN, ERROR.") + flag.BoolVar(&logFlags.verbose, "verbose", false, "Adds additional logging and prefixes logs with the [mapname]. Alias of log-level=DEBUG.") + flag.BoolVar(&logFlags.verbose, "v", false, "-verbose shorthand") + flag.BoolVar(&logFlags.performance, "log-performance", false, "Adds additional logging for performance-based recommendations, sets log-level=DEBUG") + flag.BoolVar(&logFlags.removal, "log-removal", false, "Adds additional logging of removed island and lake position/size, sets log-level=DEBUG") flag.Parse() - var level = slog.LevelInfo - - if verboseFlag || debugPerformanceFlag || debugRemovalFlag { - level = slog.LevelDebug - } - - // parse the log-level input string to the slog.Level type - if logLevelFlag != "" { - switch strings.ToLower(logLevelFlag) { - case "all": - level = LevelAll - case "debug": - level = slog.LevelDebug - case "info": - level = slog.LevelInfo - case "warn": - level = slog.LevelWarn - case "error": - level = slog.LevelError - default: - fmt.Printf("invalid log level: %s, defaulting to info\n", logLevelFlag) - level = slog.LevelInfo - } - } - logger := slog.New(NewGeneratorLogger( os.Stdout, &slog.HandlerOptions{ - Level: level, - }, - LogFlags{ - performance: debugPerformanceFlag, - removal: debugRemovalFlag, + Level: DetermineLogLevel(logFlags), }, + logFlags, )) slog.SetDefault(logger) From 686633509b48b5f882aa351dee6a0bd76f4ebf9f Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 04:20:02 -0500 Subject: [PATCH 12/17] fix operator prcedence, group prefix, and 0-tile error in map-generator logger --- map-generator/logger.go | 18 ++++++++++++++++-- map-generator/main.go | 3 ++- map-generator/map_generator.go | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/map-generator/logger.go b/map-generator/logger.go index fb2544c420..9e48a4d668 100644 --- a/map-generator/logger.go +++ b/map-generator/logger.go @@ -101,11 +101,13 @@ func (h *GeneratorLogger) Enabled(_ context.Context, level slog.Level) bool { } // Handle processes a log record. -// It decides whether to output each record based on log level and flags +// It decides whether to output each record based on log level, flags, and if the map is a test map // On output, it formats the log message with any extra formatting func (h *GeneratorLogger) Handle(_ context.Context, r slog.Record) error { isPerformanceLog := false isRemovalLog := false + isTestMap := false + var mapName string findAttrs := func(a slog.Attr) { @@ -118,6 +120,9 @@ func (h *GeneratorLogger) Handle(_ context.Context, r slog.Record) error { if a.Key == "map" { mapName = a.Value.String() } + if a.Key == "isTest" { + isTestMap = a.Value.Bool() + } } // Check record attributes for performance tag and map name @@ -140,10 +145,15 @@ func (h *GeneratorLogger) Handle(_ context.Context, r slog.Record) error { return nil } + // dont log performance messages for test maps + if isPerformanceLog && isTestMap { + return nil + } + buf := &bytes.Buffer{} // Add map name as a prefix in log Level DEBUG and ALL - if h.opts.Level == slog.LevelDebug || h.opts.Level == LevelAll && mapName != "" { + if (h.opts.Level == slog.LevelDebug || h.opts.Level == LevelAll) && mapName != "" { mapName = strings.Trim(mapName, `"`) fmt.Fprintf(buf, "[%s] ", mapName) } @@ -153,6 +163,10 @@ func (h *GeneratorLogger) Handle(_ context.Context, r slog.Record) error { fmt.Fprintf(buf, "[PERF] ") } + if h.prefix != "" { + fmt.Fprintf(buf, "%s ", h.prefix) + } + fmt.Fprintln(buf, r.Message) h.mu.Lock() diff --git a/map-generator/main.go b/map-generator/main.go index bdb1f7de30..39ffc4b006 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -228,7 +228,8 @@ func loadTerrainMaps() error { go func() { defer wg.Done() var MapLogTag = slog.String("map", mapItem.Name) - logger := slog.Default().With(MapLogTag) + var TestLogTag = slog.Bool("isTest", mapItem.IsTest) + logger := slog.Default().With(MapLogTag).With(TestLogTag) ctx := ContextWithLogger(context.Background(), logger) if err := processMap(ctx, mapItem.Name, mapItem.IsTest); err != nil { errChan <- err diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index a47e7fc2ab..0bb3e3358c 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -179,7 +179,7 @@ func GenerateMap(ctx context.Context, args GeneratorArgs) (MapResult, error) { logger.Debug(fmt.Sprintf("Land Tile Count (16x): %d", numLandTiles16x)) if mapNumLandTiles == 0 { - logger.Error("Map has 0 land tiles") + return MapResult{}, fmt.Errorf("Map has 0 land tiles") } if mapNumLandTiles > maxRecommendedLandTileCount { logger.Info(fmt.Sprintf("Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) From a9ed7a0b87e58d217862d6906043a3c147b249da Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 05:00:40 -0500 Subject: [PATCH 13/17] fix comment --- map-generator/logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map-generator/logger.go b/map-generator/logger.go index 9e48a4d668..204158b119 100644 --- a/map-generator/logger.go +++ b/map-generator/logger.go @@ -64,7 +64,7 @@ func DetermineLogLevel( return level } -// GeneratorLogger is a custom slog.Handler that outputs logs based on log level and performance flags. +// GeneratorLogger is a custom slog.Handler that outputs logs based on log level and additional LogFlags. type GeneratorLogger struct { opts slog.HandlerOptions w io.Writer From 8cd452657b2675c0132a99562c7ef856b3876f70 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 05:06:29 -0500 Subject: [PATCH 14/17] fix const block --- map-generator/map_generator.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 0bb3e3358c..8caf5405e9 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -12,13 +12,10 @@ import ( "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 -) - -const ( // the recommended max area pixel size for input images minRecommendedPixelSize = 2000000 maxRecommendedPixelSize = 3000000 From 75a3645f2a8ef88be2a97a4eb6f617484be49da1 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Mon, 29 Dec 2025 05:35:15 -0500 Subject: [PATCH 15/17] ensure logger calls with tags use Debug --- map-generator/map_generator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/map-generator/map_generator.go b/map-generator/map_generator.go index 8caf5405e9..1c0dffdd30 100644 --- a/map-generator/map_generator.go +++ b/map-generator/map_generator.go @@ -117,7 +117,7 @@ func GenerateMap(ctx context.Context, args GeneratorArgs) (MapResult, error) { area := width * height if area < minRecommendedPixelSize || area > maxRecommendedPixelSize { - logger.Info(fmt.Sprintf("Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize), PerformanceLogTag) + logger.Debug(fmt.Sprintf("Map area %d pixels is outside recommended range (%d - %d)", area, minRecommendedPixelSize, maxRecommendedPixelSize), PerformanceLogTag) } // Initialize terrain grid @@ -179,7 +179,7 @@ func GenerateMap(ctx context.Context, args GeneratorArgs) (MapResult, error) { return MapResult{}, fmt.Errorf("Map has 0 land tiles") } if mapNumLandTiles > maxRecommendedLandTileCount { - logger.Info(fmt.Sprintf("Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) + logger.Debug(fmt.Sprintf("Map has more land tiles (%d) than recommended maximum (%d)", mapNumLandTiles, maxRecommendedLandTileCount), PerformanceLogTag) } return MapResult{ From d9292ac1453e2d1a2454ad7ae14539f09af23d37 Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Thu, 8 Jan 2026 19:44:38 -0500 Subject: [PATCH 16/17] Update map-generator/main.go update var naming to go conventions Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- map-generator/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/map-generator/main.go b/map-generator/main.go index 55e178c88e..f8082851c4 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -233,8 +233,9 @@ func loadTerrainMaps() error { mapItem := mapItem go func() { defer wg.Done() - var MapLogTag = slog.String("map", mapItem.Name) - var TestLogTag = slog.Bool("isTest", mapItem.IsTest) + mapLogTag := slog.String("map", mapItem.Name) + testLogTag := slog.Bool("isTest", mapItem.IsTest) + logger := slog.Default().With(mapLogTag).With(testLogTag) logger := slog.Default().With(MapLogTag).With(TestLogTag) ctx := ContextWithLogger(context.Background(), logger) if err := processMap(ctx, mapItem.Name, mapItem.IsTest); err != nil { From 27d8a00781c1314e965c33d645699b13f6e319ed Mon Sep 17 00:00:00 2001 From: Aaron Tidwell Date: Thu, 8 Jan 2026 20:36:57 -0500 Subject: [PATCH 17/17] fix coderabbits bad commit --- map-generator/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/map-generator/main.go b/map-generator/main.go index f8082851c4..88250b24cb 100644 --- a/map-generator/main.go +++ b/map-generator/main.go @@ -236,7 +236,6 @@ func loadTerrainMaps() error { mapLogTag := slog.String("map", mapItem.Name) testLogTag := slog.Bool("isTest", mapItem.IsTest) logger := slog.Default().With(mapLogTag).With(testLogTag) - logger := slog.Default().With(MapLogTag).With(TestLogTag) ctx := ContextWithLogger(context.Background(), logger) if err := processMap(ctx, mapItem.Name, mapItem.IsTest); err != nil { errChan <- err