Add format:map-generator command (#2563)

I'd like to submit some PRs in the future to add additional options for
logging and debugging to the map-generator, however I don't want to
introduce any code style changes in feature requests.

## Description:

I noticed there was not any style guide or formatting being done for the
go files, and went with the simplest built-in approach w/ [go
fmt](https://go.dev/blog/gofmt)

- Adds a `format:map-generator` npm command that will run the default
`go fmt` in the `map-generator` directory to format the go code.
- Runs the formatter and commits the changes to `main.go` and
`map_generator.go`
- Updates the `map-generator` README:
  - Updated location for generated files
  - Updated Links to use markdown links
  - Add `info.json` example
  - Add in-game enabling instructions with list of files to modify
  - Add development tools section with format command

## Please complete the following:

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

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

tidwell
This commit is contained in:
Aaron Tidwell
2025-12-09 22:46:13 -05:00
committed by GitHub
parent 089d9ab402
commit 2bfe245a72
4 changed files with 159 additions and 124 deletions
+106 -106
View File
@@ -43,20 +43,20 @@ type Terrain struct {
type MapResult struct {
Thumbnail []byte
Map MapInfo
Map4x MapInfo
Map16x MapInfo
Map MapInfo
Map4x MapInfo
Map16x MapInfo
}
type MapInfo struct {
Data []byte
Width int
Height int
Data []byte
Width int
Height int
NumLandTiles int
}
type GeneratorArgs struct {
Name string
Name string
ImageBuffer []byte
RemoveSmall bool
}
@@ -96,7 +96,7 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
} else {
// Land
terrain[x][y] = Terrain{Type: Land}
// Calculate magnitude from blue channel (140-200 range)
mag := math.Min(200, math.Max(140, float64(blue))) - 140
terrain[x][y].Magnitude = mag / 2
@@ -108,11 +108,11 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
processWater(terrain, args.RemoveSmall)
terrain4x := createMiniMap(terrain)
processWater(terrain4x, false)
processWater(terrain4x, false)
terrain16x := createMiniMap(terrain4x)
processWater(terrain16x, false)
thumb := createMapThumbnail(terrain4x, 0.5)
webp, err := convertToWebP(ThumbData{
Data: thumb.Pix,
@@ -129,21 +129,21 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
return MapResult{
Map: MapInfo{
Data: mapData,
Width: width,
Height: height,
Data: mapData,
Width: width,
Height: height,
NumLandTiles: mapNumLandTiles,
},
Map4x: MapInfo{
Data: mapData4x,
Width: width / 2,
Height: height / 2,
Data: mapData4x,
Width: width / 2,
Height: height / 2,
NumLandTiles: numLandTiles4x,
},
Map16x: MapInfo{
Data: mapData16x,
Width: width / 4,
Height: height / 4,
Data: mapData16x,
Width: width / 4,
Height: height / 4,
NumLandTiles: numLandTiles16x,
},
Thumbnail: webp,
@@ -153,13 +153,13 @@ func GenerateMap(args GeneratorArgs) (MapResult, error) {
func convertToWebP(thumb ThumbData) ([]byte, error) {
// Create RGBA image from raw data
img := image.NewRGBA(image.Rect(0, 0, thumb.Width, thumb.Height))
// Copy the raw RGBA data
if len(thumb.Data) != thumb.Width*thumb.Height*4 {
return nil, fmt.Errorf("invalid thumb data length: expected %d, got %d",
return nil, fmt.Errorf("invalid thumb data length: expected %d, got %d",
thumb.Width*thumb.Height*4, len(thumb.Data))
}
copy(img.Pix, thumb.Data)
// Encode as WebP with quality 45 (equivalent to the JavaScript version)
@@ -174,10 +174,10 @@ func convertToWebP(thumb ThumbData) ([]byte, error) {
func createMiniMap(tm [][]Terrain) [][]Terrain {
width := len(tm)
height := len(tm[0])
miniWidth := width / 2
miniHeight := height / 2
miniMap := make([][]Terrain, miniWidth)
for x := range miniMap {
miniMap[x] = make([]Terrain, miniHeight)
@@ -187,7 +187,7 @@ func createMiniMap(tm [][]Terrain) [][]Terrain {
for y := 0; y < height; y++ {
miniX := x / 2
miniY := y / 2
if miniX < miniWidth && miniY < miniHeight {
// If any of the 4 tiles has water, mini tile is water
if miniMap[miniX][miniY].Type != Water {
@@ -196,7 +196,7 @@ func createMiniMap(tm [][]Terrain) [][]Terrain {
}
}
}
return miniMap
}
@@ -210,7 +210,7 @@ func processShore(terrain [][]Terrain) []Coord {
for y := 0; y < height; y++ {
tile := &terrain[x][y]
neighbors := getNeighbors(x, y, terrain)
if tile.Type == Land {
// Land tile adjacent to water is shoreline
for _, n := range neighbors {
@@ -231,47 +231,47 @@ func processShore(terrain [][]Terrain) []Coord {
}
}
}
return shorelineWaters
}
func processDistToLand(shorelineWaters []Coord, terrain [][]Terrain) {
log.Println("Setting Water tiles magnitude = Manhattan distance from nearest land")
width := len(terrain)
height := len(terrain[0])
visited := make([][]bool, width)
for x := range visited {
visited[x] = make([]bool, height)
}
type queueItem struct {
x, y, dist int
}
queue := make([]queueItem, 0)
// Initialize queue with shoreline waters
for _, coord := range shorelineWaters {
queue = append(queue, queueItem{x: coord.X, y: coord.Y, dist: 0})
visited[coord.X][coord.Y] = true
terrain[coord.X][coord.Y].Magnitude = 0
}
directions := []Coord{{0, 1}, {1, 0}, {0, -1}, {-1, 0}}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
for _, dir := range directions {
nx := current.x + dir.X
ny := current.y + dir.Y
if nx >= 0 && ny >= 0 && nx < width && ny < height &&
!visited[nx][ny] && terrain[nx][ny].Type == Water {
visited[nx][ny] = true
terrain[nx][ny].Magnitude = float64(current.dist + 1)
queue = append(queue, queueItem{x: nx, y: ny, dist: current.dist + 1})
@@ -293,7 +293,7 @@ func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord {
width := len(terrain)
height := len(terrain[0])
var coords []Coord
if x > 0 {
coords = append(coords, Coord{X: x - 1, Y: y})
}
@@ -306,21 +306,21 @@ func getNeighborCoords(x, y int, terrain [][]Terrain) []Coord {
if y < height-1 {
coords = append(coords, Coord{X: x, Y: y + 1})
}
return coords
}
func processWater(terrain [][]Terrain, removeSmall bool) {
log.Println("Processing water bodies")
visited := make(map[string]bool)
type waterBody struct {
coords []Coord
size int
}
var waterBodies []waterBody
// Find all distinct water bodies
for x := 0; x < len(terrain); x++ {
for y := 0; y < len(terrain[0]); y++ {
@@ -329,7 +329,7 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
if visited[key] {
continue
}
coords := getArea(x, y, terrain, visited)
waterBodies = append(waterBodies, waterBody{
coords: coords,
@@ -338,7 +338,7 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
}
}
}
// Sort by size (largest first)
for i := 0; i < len(waterBodies)-1; i++ {
for j := i + 1; j < len(waterBodies); j++ {
@@ -347,9 +347,9 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
}
}
}
smallLakes := 0
if len(waterBodies) > 0 {
// Mark largest water body as ocean
largestWaterBody := waterBodies[0]
@@ -357,7 +357,7 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
terrain[coord.X][coord.Y].Ocean = true
}
log.Printf("Identified ocean with %d water tiles", largestWaterBody.size)
if removeSmall {
// Remove small water bodies
log.Println("Searching for small water bodies for removal")
@@ -370,10 +370,10 @@ func processWater(terrain [][]Terrain, removeSmall bool) {
}
}
}
log.Printf("Identified and removed %d bodies of water smaller than %d tiles",
log.Printf("Identified and removed %d bodies of water smaller than %d tiles",
smallLakes, minLakeSize)
}
// Process shorelines and distances
shorelineWaters := processShore(terrain)
processDistToLand(shorelineWaters, terrain)
@@ -386,25 +386,25 @@ func getArea(x, y int, terrain [][]Terrain, visited map[string]bool) []Coord {
targetType := terrain[x][y].Type
var area []Coord
queue := []Coord{{X: x, Y: y}}
for len(queue) > 0 {
coord := queue[0]
queue = queue[1:]
key := fmt.Sprintf("%d,%d", coord.X, coord.Y)
if visited[key] {
continue
}
visited[key] = true
if terrain[coord.X][coord.Y].Type == targetType {
area = append(area, coord)
neighborCoords := getNeighborCoords(coord.X, coord.Y, terrain)
queue = append(queue, neighborCoords...)
}
}
return area
}
@@ -412,16 +412,16 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
if !removeSmall {
return
}
visited := make(map[string]bool)
type landBody struct {
coords []Coord
size int
}
var landBodies []landBody
// Find all distinct land bodies
for x := 0; x < len(terrain); x++ {
for y := 0; y < len(terrain[0]); y++ {
@@ -430,7 +430,7 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
if visited[key] {
continue
}
coords := getArea(x, y, terrain, visited)
landBodies = append(landBodies, landBody{
coords: coords,
@@ -439,9 +439,9 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
}
}
}
smallIslands := 0
for _, body := range landBodies {
if body.size < minIslandSize {
smallIslands++
@@ -451,8 +451,8 @@ func removeSmallIslands(terrain [][]Terrain, removeSmall bool) {
}
}
}
log.Printf("Identified and removed %d islands smaller than %d tiles",
log.Printf("Identified and removed %d islands smaller than %d tiles",
smallIslands, minIslandSize)
}
@@ -461,12 +461,12 @@ func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
height := len(terrain[0])
packedData := make([]byte, width*height)
numLandTiles = 0
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
tile := terrain[x][y]
var packedByte byte = 0
if tile.Type == Land {
packedByte |= 0b10000000
numLandTiles++
@@ -477,46 +477,46 @@ func packTerrain(terrain [][]Terrain) (data []byte, numLandTiles int) {
if tile.Ocean {
packedByte |= 0b00100000
}
if tile.Type == Land {
packedByte |= byte(math.Min(math.Ceil(tile.Magnitude), 31))
} else {
packedByte |= byte(math.Min(math.Ceil(tile.Magnitude/2), 31))
}
packedData[y*width+x] = packedByte
}
}
logBinaryAsBits(packedData, 8)
return packedData, numLandTiles
}
func createMapThumbnail(terrain [][]Terrain, quality float64) *image.RGBA {
log.Println("Creating thumbnail")
srcWidth := len(terrain)
srcHeight := len(terrain[0])
targetWidth := int(math.Max(1, math.Floor(float64(srcWidth)*quality)))
targetHeight := int(math.Max(1, math.Floor(float64(srcHeight)*quality)))
img := image.NewRGBA(image.Rect(0, 0, targetWidth, targetHeight))
for x := 0; x < targetWidth; x++ {
for y := 0; y < targetHeight; y++ {
srcX := int(math.Floor(float64(x) / quality))
srcY := int(math.Floor(float64(y) / quality))
srcX = int(math.Min(float64(srcX), float64(srcWidth-1)))
srcY = int(math.Min(float64(srcY), float64(srcHeight-1)))
terrain := terrain[srcX][srcY]
rgba := getThumbnailColor(terrain)
img.Set(x, y, color.RGBA{R: rgba.R, G: rgba.G, B: rgba.B, A: rgba.A})
}
}
return img
}
@@ -539,12 +539,12 @@ func getThumbnailColor(t Terrain) RGBA {
A: 0,
}
}
// Shoreline land
if t.Shoreline {
return RGBA{R: 204, G: 203, B: 158, A: 255}
}
var adjRGB float64
if t.Magnitude < 10 {
// Plains
@@ -580,7 +580,7 @@ func logBinaryAsBits(data []byte, length int) {
if length > len(data) {
length = len(data)
}
var bits string
for i := 0; i < length; i++ {
bits += fmt.Sprintf("%08b ", data[i])
@@ -593,7 +593,7 @@ func createCombinedBinary(infoBuffer []byte, mapData []byte, miniMapData []byte)
infoSize := len(infoBuffer)
mapSize := len(mapData)
miniMapSize := len(miniMapData)
// Header structure:
// Bytes 0-3: Version (1)
// Bytes 4-7: Info section offset (always 28)
@@ -602,35 +602,35 @@ func createCombinedBinary(infoBuffer []byte, mapData []byte, miniMapData []byte)
// Bytes 16-19: Map section size
// Bytes 20-23: MiniMap section offset
// Bytes 24-27: MiniMap section size
headerSize := 28
infoOffset := headerSize
mapOffset := infoOffset + infoSize
miniMapOffset := mapOffset + mapSize
totalSize := miniMapOffset + miniMapSize
combined := make([]byte, totalSize)
// Write version
writeUint32(combined, 0, 1)
// Write info section info
writeUint32(combined, 4, uint32(infoOffset))
writeUint32(combined, 8, uint32(infoSize))
// Write map section info
writeUint32(combined, 12, uint32(mapOffset))
writeUint32(combined, 16, uint32(mapSize))
// Write miniMap section info
writeUint32(combined, 20, uint32(miniMapOffset))
writeUint32(combined, 24, uint32(miniMapSize))
// Copy data sections
copy(combined[infoOffset:], infoBuffer)
copy(combined[mapOffset:], mapData)
copy(combined[miniMapOffset:], miniMapData)
return combined
}
@@ -649,38 +649,38 @@ func decodeCombinedBinary(data []byte) (*CombinedBinaryHeader, []byte, []byte, [
if len(data) < 28 {
return nil, nil, nil, nil, fmt.Errorf("data too short for header")
}
header := &CombinedBinaryHeader{
Version: readUint32(data, 0),
InfoOffset: readUint32(data, 4),
InfoSize: readUint32(data, 8),
MapOffset: readUint32(data, 12),
MapSize: readUint32(data, 16),
Version: readUint32(data, 0),
InfoOffset: readUint32(data, 4),
InfoSize: readUint32(data, 8),
MapOffset: readUint32(data, 12),
MapSize: readUint32(data, 16),
MiniMapOffset: readUint32(data, 20),
MiniMapSize: readUint32(data, 24),
MiniMapSize: readUint32(data, 24),
}
// Validate offsets and sizes
if header.InfoOffset+header.InfoSize > uint32(len(data)) ||
header.MapOffset+header.MapSize > uint32(len(data)) ||
header.MiniMapOffset+header.MiniMapSize > uint32(len(data)) {
return nil, nil, nil, nil, fmt.Errorf("invalid offsets or sizes in header")
}
// Extract sections
infoData := data[header.InfoOffset : header.InfoOffset+header.InfoSize]
mapData := data[header.MapOffset : header.MapOffset+header.MapSize]
miniMapData := data[header.MiniMapOffset : header.MiniMapOffset+header.MiniMapSize]
return header, infoData, mapData, miniMapData, nil
}
type CombinedBinaryHeader struct {
Version uint32
InfoOffset uint32
InfoSize uint32
MapOffset uint32
MapSize uint32
Version uint32
InfoOffset uint32
InfoSize uint32
MapOffset uint32
MapSize uint32
MiniMapOffset uint32
MiniMapSize uint32
}
MiniMapSize uint32
}