Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c72354f
Add -verbose and -performance flags to map generator
Tidwell Dec 28, 2025
997861a
shuffle location of logger type and abstract logger creation fn
Tidwell Dec 28, 2025
ff7744f
rename fn arg so it does not shadow type name
Tidwell Dec 28, 2025
05f931c
update nitpick comments in map generator logging
Tidwell Dec 28, 2025
dd4b69c
unexport and rename LogArgs to loggerArgs type
Tidwell Dec 28, 2025
7ee6a8d
working custom slog formater
Tidwell Dec 29, 2025
315de9e
abstract to slog logger and clean up log level logic
Tidwell Dec 29, 2025
798fffc
clean up readme and comments
Tidwell Dec 29, 2025
da4c70b
update docs
Tidwell Dec 29, 2025
ecba038
switch to context-based passing for logger
Tidwell Dec 29, 2025
60a193d
further isolate logger related logic
Tidwell Dec 29, 2025
6866335
fix operator prcedence, group prefix, and 0-tile error in map-generat…
Tidwell Dec 29, 2025
9f1541e
Merge branch 'main' into map-generator-verbose
Tidwell Dec 29, 2025
a9ed7a0
fix comment
Tidwell Dec 29, 2025
8cd4526
fix const block
Tidwell Dec 29, 2025
75a3645
ensure logger calls with tags use Debug
Tidwell Dec 29, 2025
02556b4
Merge branch 'main' into map-generator-verbose
Tidwell Dec 29, 2025
d995069
Merge branch 'main' into map-generator-verbose
Tidwell Dec 29, 2025
2c53f51
Merge branch 'main' into map-generator-verbose
Tidwell Dec 31, 2025
fc75d37
Merge branch 'main' into map-generator-verbose
Tidwell Jan 7, 2026
239da1d
Merge branch 'main' into map-generator-verbose
Tidwell Jan 9, 2026
d9292ac
Update map-generator/main.go
Tidwell Jan 9, 2026
27d8a00
fix coderabbits bad commit
Tidwell Jan 9, 2026
2fc2dfc
Merge branch 'main' into map-generator-verbose
Tidwell Jan 9, 2026
05f3132
Merge branch 'main' into map-generator-verbose
Tidwell Jan 9, 2026
844391b
Merge branch 'main' into map-generator-verbose
Tidwell Jan 9, 2026
74cb9e3
Merge branch 'main' into map-generator-verbose
Tidwell Jan 10, 2026
e4ae4c0
Merge branch 'main' into map-generator-verbose
Tidwell Jan 12, 2026
fcbeb96
Merge branch 'main' into map-generator-verbose
Tidwell Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions map-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ To process a subset of maps, pass a comma-separated list:
- `../resources/maps/<map_name>/map16x.bin` - 1/16 scale (quarter dimensions) binary map data used for mini-maps.
- `../resources/maps/<map_name>/thumbnail.webp` - WebP image thumbnail of the map.

## Command Line Flags

- `--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

The map-generator will process your input file at `assets/maps/<map_name>/image.png` to generate the map
Expand Down
213 changes: 213 additions & 0 deletions map-generator/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// This is the custom logger providing the multi-level and flag-based logging for
// the map-generator. It uses slog.
package main

import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"strings"
"sync"
)

type LogFlags struct {
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 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.
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 additional LogFlags.
type GeneratorLogger struct {
opts slog.HandlerOptions
w io.Writer
mu *sync.Mutex
attrs []slog.Attr
prefix string
flags LogFlags
}

// NewGeneratorLogger creates a new GeneratorLogger.
// It initializes a handler with specific output, options, and flags
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
}
if h.opts.Level == nil {
h.opts.Level = slog.LevelInfo
}
return h
}

// 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()
}

// Handle processes a log record.
// 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) {
if a.Equal(PerformanceLogTag) {
isPerformanceLog = true
}
if a.Equal(RemovalLogTag) {
isRemovalLog = true
}
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
r.Attrs(func(a slog.Attr) bool {
findAttrs(a)
return true
})

// Check handler's own attributes for performance tag and map name
for _, a := range h.attrs {
findAttrs(a)
}

// 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
}

// 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 != "" {
mapName = strings.Trim(mapName, `"`)
fmt.Fprintf(buf, "[%s] ", mapName)
}

// Add prefix for performance messages
if isPerformanceLog {
fmt.Fprintf(buf, "[PERF] ")
}

if h.prefix != "" {
fmt.Fprintf(buf, "%s ", h.prefix)
}

fmt.Fprintln(buf, r.Message)

h.mu.Lock()
defer h.mu.Unlock()
_, err := h.w.Write(buf.Bytes())
return err
}

// 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
}

// 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
}
newHandler := *h
if newHandler.prefix != "" {
newHandler.prefix += "."
}
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)
}
36 changes: 30 additions & 6 deletions map-generator/main.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
)

// mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument.
var mapsFlag 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.
//
Expand Down Expand Up @@ -75,6 +74,12 @@ var maps = []struct {
{Name: "world", IsTest: true},
}

// mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument.
var mapsFlag string

// 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.
func outputMapDir(isTest bool) (string, error) {
Expand Down Expand Up @@ -104,7 +109,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)
Expand Down Expand Up @@ -135,7 +140,7 @@ func processMap(name string, isTest bool) error {
}

// 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,
Expand Down Expand Up @@ -230,7 +235,11 @@ func loadTerrainMaps() error {
mapItem := mapItem
go func() {
defer wg.Done()
if err := processMap(mapItem.Name, mapItem.IsTest); err != nil {
mapLogTag := slog.String("map", mapItem.Name)
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
}
}()
Expand All @@ -254,8 +263,23 @@ 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(&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()

logger := slog.New(NewGeneratorLogger(
os.Stdout,
&slog.HandlerOptions{
Level: DetermineLogLevel(logFlags),
},
logFlags,
))

slog.SetDefault(logger)

if err := loadTerrainMaps(); err != nil {
log.Fatalf("Error generating terrain maps: %v", err)
}
Expand Down
Loading
Loading