mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 18:43:07 +00:00
2cb5244ad4
## Description: "A high-stakes naval theater where empires clash over narrow corridors, bottleneck straits, and heavily fortified shorelines." Modeled to the exact strategic proportions of the classic Black Sea map, Yellow Sea shifts the focus of global conflict to East Asia. The map is defined by its massive central body of water, making naval dominance absolutely essential for survival. However, unlike wide-open oceans, control of the Yellow Sea is entirely dictated by its unique coastal geography. The Shandong And Liaoning Peninsulas are The definitive feature of the map. Two massive, opposing peninsulas project deep into the sea, acting as natural, heavily contestable daggers. They create tight naval choke points in the central waters while forcing land-based players into brutal, linear frontlines where every pixel of territory is bought with blood. The Continental Rim: A sprawling mainland coast wraps around the northern and western edges of the map, offering expansive land routes for players who prefer sweeping land invasions over amphibious assaults. Scale Class: Medium Gameplay Style: Naval/Land Hybrid, Tactical Choke Points, Frontline Bottlenecks Nations: 8 North Korea South Korea Liaoning Shandong Beijing Hebei Tianjin Jilin description mostly generated by google gemini ai ## 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: DISCORD_USERNAME crunchybbbbb <img width="1660" height="1266" alt="Screenshot 2026-05-24 220103" src="https://github.com/user-attachments/assets/800c6732-677d-44f1-ba5c-c60da5f199e0" /> <img width="1500" height="1152" alt="yellow_sea2" src="https://github.com/user-attachments/assets/9b3ba34a-3f9c-4485-9235-f953fd07be4c" /> Game play video https://youtu.be/IcRPTM0rHM0 --------- Co-authored-by: RickD004 <realtacoco@gmail.com>
340 lines
10 KiB
Go
340 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// 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.
|
|
//
|
|
// New maps need to be added here in order to allow the map-generator to process them.
|
|
var maps = []struct {
|
|
Name string
|
|
IsTest bool
|
|
}{
|
|
{Name: "achiran"},
|
|
{Name: "aegean"},
|
|
{Name: "africa"},
|
|
{Name: "alps"},
|
|
{Name: "amazonriver"},
|
|
{Name: "antarctica"},
|
|
{Name: "archipelagosea"},
|
|
{Name: "arctic"},
|
|
{Name: "asia"},
|
|
{Name: "australia"},
|
|
{Name: "baikal"},
|
|
{Name: "baikalnukewars"},
|
|
{Name: "bajacalifornia"},
|
|
{Name: "balkans"},
|
|
{Name: "beringsea"},
|
|
{Name: "beringstrait"},
|
|
{Name: "betweentwoseas"},
|
|
{Name: "blacksea"},
|
|
{Name: "bosphorusstraits"},
|
|
{Name: "britannia"},
|
|
{Name: "britanniaclassic"},
|
|
{Name: "caucasus"},
|
|
{Name: "conakry"},
|
|
{Name: "danishstraits"},
|
|
{Name: "deglaciatedantarctica"},
|
|
{Name: "didier"},
|
|
{Name: "didierfrance"},
|
|
{Name: "dyslexdria"},
|
|
{Name: "eastasia"},
|
|
{Name: "europe"},
|
|
{Name: "europeclassic"},
|
|
{Name: "falklandislands"},
|
|
{Name: "faroeislands"},
|
|
{Name: "fourislands"},
|
|
{Name: "gatewaytotheatlantic"},
|
|
{Name: "giantworldmap"},
|
|
{Name: "greatlakes"},
|
|
{Name: "gulfofstlawrence"},
|
|
{Name: "halkidiki"},
|
|
{Name: "hawaii"},
|
|
{Name: "iceland"},
|
|
{Name: "indiansubcontinent"},
|
|
{Name: "italia"},
|
|
{Name: "japan"},
|
|
{Name: "korea"},
|
|
{Name: "labyrinth"},
|
|
{Name: "lemnos"},
|
|
{Name: "lisbon"},
|
|
{Name: "losangeles"},
|
|
{Name: "luna"},
|
|
{Name: "manicouagan"},
|
|
{Name: "marenostrum"},
|
|
{Name: "mars"},
|
|
{Name: "mena"},
|
|
{Name: "middleeast"},
|
|
{Name: "milkyway"},
|
|
{Name: "montreal"},
|
|
{Name: "newyorkcity"},
|
|
{Name: "niledelta"},
|
|
{Name: "northamerica"},
|
|
{Name: "northwestpassage"},
|
|
{Name: "oceania"},
|
|
{Name: "pangaea"},
|
|
{Name: "passage"},
|
|
{Name: "pluto"},
|
|
{Name: "sanfrancisco"},
|
|
{Name: "sierpinski"},
|
|
{Name: "southamerica"},
|
|
{Name: "straitofgibraltar"},
|
|
{Name: "straitofhormuz"},
|
|
{Name: "straitofmalacca"},
|
|
{Name: "surrounded"},
|
|
{Name: "svalmel"},
|
|
{Name: "taiwanstrait"},
|
|
{Name: "thebox"},
|
|
{Name: "tourney1"},
|
|
{Name: "tourney2"},
|
|
{Name: "tourney3"},
|
|
{Name: "tourney4"},
|
|
{Name: "tradersdream"},
|
|
{Name: "twolakes"},
|
|
{Name: "venice"},
|
|
{Name: "world"},
|
|
{Name: "yellowsea"},
|
|
{Name: "yenisei"},
|
|
{Name: "big_plains", IsTest: true},
|
|
{Name: "half_land_half_ocean", IsTest: true},
|
|
{Name: "ocean_and_land", IsTest: true},
|
|
{Name: "plains", IsTest: true},
|
|
{Name: "giantworldmap", IsTest: true},
|
|
{Name: "world", IsTest: true},
|
|
}
|
|
|
|
// mapsFlag holds the comma-separated list of map names passed via the --maps command-line argument.
|
|
var mapsFlag string
|
|
|
|
// workersFlag controls how many maps are processed concurrently, bounding peak memory usage.
|
|
var workersFlag int
|
|
|
|
// 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) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get working directory: %w", err)
|
|
}
|
|
if isTest {
|
|
return filepath.Join(cwd, "..", "tests", "testdata", "maps"), nil
|
|
}
|
|
return filepath.Join(cwd, "..", "resources", "maps"), nil
|
|
}
|
|
|
|
// inputMapDir returns the absolute path to the directory containing source map assets.
|
|
// It distinguishes between test and production asset locations.
|
|
func inputMapDir(isTest bool) (string, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get working directory: %w", err)
|
|
}
|
|
if isTest {
|
|
return filepath.Join(cwd, "assets", "test_maps"), nil
|
|
} else {
|
|
return filepath.Join(cwd, "assets", "maps"), nil
|
|
}
|
|
}
|
|
|
|
// 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(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)
|
|
}
|
|
|
|
inputMapDir, err := inputMapDir(isTest)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get input map directory: %w", err)
|
|
}
|
|
|
|
inputPath := filepath.Join(inputMapDir, name, "image.png")
|
|
imageBuffer, err := os.ReadFile(inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read map file %s: %w", inputPath, err)
|
|
}
|
|
|
|
// Read the info.json file
|
|
manifestPath := filepath.Join(inputMapDir, name, "info.json")
|
|
manifestBuffer, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read info file %s: %w", manifestPath, err)
|
|
}
|
|
|
|
// Parse the info buffer as dynamic JSON
|
|
var manifest map[string]interface{}
|
|
if err := json.Unmarshal(manifestBuffer, &manifest); err != nil {
|
|
return fmt.Errorf("failed to parse info.json for %s: %w", name, err)
|
|
}
|
|
|
|
// Generate maps
|
|
result, err := GenerateMap(ctx, GeneratorArgs{
|
|
ImageBuffer: imageBuffer,
|
|
RemoveSmall: !isTest, // Don't remove small islands for test maps
|
|
Name: name,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate map for %s: %w", name, err)
|
|
}
|
|
|
|
manifest["map"] = map[string]interface{}{
|
|
"width": result.Map.Width,
|
|
"height": result.Map.Height,
|
|
"num_land_tiles": result.Map.NumLandTiles,
|
|
}
|
|
manifest["map4x"] = map[string]interface{}{
|
|
"width": result.Map4x.Width,
|
|
"height": result.Map4x.Height,
|
|
"num_land_tiles": result.Map4x.NumLandTiles,
|
|
}
|
|
manifest["map16x"] = map[string]interface{}{
|
|
"width": result.Map16x.Width,
|
|
"height": result.Map16x.Height,
|
|
"num_land_tiles": result.Map16x.NumLandTiles,
|
|
}
|
|
|
|
mapDir := filepath.Join(outputMapBaseDir, name)
|
|
if err := os.MkdirAll(mapDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create output directory for %s: %w", name, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(mapDir, "map.bin"), result.Map.Data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write combined binary for %s: %w", name, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(mapDir, "map4x.bin"), result.Map4x.Data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write combined binary for %s: %w", name, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(mapDir, "map16x.bin"), result.Map16x.Data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write combined binary for %s: %w", name, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(mapDir, "thumbnail.webp"), result.Thumbnail, 0644); err != nil {
|
|
return fmt.Errorf("failed to write thumbnail for %s: %w", name, err)
|
|
}
|
|
|
|
// Serialize the updated manifest to JSON
|
|
updatedManifest, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize manifest for %s: %w", name, err)
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(mapDir, "manifest.json"), updatedManifest, 0644); err != nil {
|
|
return fmt.Errorf("failed to write manifest for %s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseMapsFlag validates and parses the --maps command-line argument.
|
|
// It returns a set of selected map names or nil if no flag was provided (implying all maps).
|
|
func parseMapsFlag() (map[string]bool, error) {
|
|
if mapsFlag == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
validNames := make(map[string]bool, len(maps))
|
|
for _, m := range maps {
|
|
validNames[m.Name] = true
|
|
}
|
|
|
|
selected := make(map[string]bool)
|
|
for _, name := range strings.Split(mapsFlag, ",") {
|
|
if !validNames[name] {
|
|
return nil, fmt.Errorf("map %q is not defined", name)
|
|
}
|
|
selected[name] = true
|
|
}
|
|
return selected, nil
|
|
}
|
|
|
|
// loadTerrainMaps manages the concurrent generation of all selected maps.
|
|
// It spins up goroutines for each map and aggregates any errors.
|
|
// Concurrency is bounded by --workers to cap peak memory usage.
|
|
func loadTerrainMaps() error {
|
|
if workersFlag < 1 {
|
|
return fmt.Errorf("--workers must be >= 1, got %d", workersFlag)
|
|
}
|
|
selectedMaps, err := parseMapsFlag()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var wg sync.WaitGroup
|
|
errChan := make(chan error, len(maps))
|
|
sem := make(chan struct{}, workersFlag)
|
|
|
|
// Process maps concurrently, bounded by the semaphore
|
|
for _, mapItem := range maps {
|
|
if selectedMaps != nil && !selectedMaps[mapItem.Name] {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
mapItem := mapItem
|
|
go func() {
|
|
defer wg.Done()
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
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
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
wg.Wait()
|
|
close(errChan)
|
|
|
|
// Check for errors
|
|
for err := range errChan {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// main is the entry point for the map generator tool.
|
|
// 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.IntVar(&workersFlag, "workers", 4, "number of maps to process concurrently. reduce to lower peak memory usage.")
|
|
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)
|
|
}
|
|
|
|
fmt.Println("Terrain maps generated successfully")
|
|
}
|