package main import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" ) // categoryOrder defines the set of valid "categories" values in info.json and // the display order of map categories in the generated TypeScript. var categoryOrder = []string{ "featured", "world", "europe", "asia", "north_america", "africa", "south_america", "oceania", "antarctica", "cosmic", "tournament", "other", } // mapInfo is the subset of info.json fields used for TypeScript generation. type mapInfo struct { ID string `json:"id"` Name string `json:"name"` TranslationKey string `json:"translation_key"` Categories []string `json:"categories"` // How many times the map appears in the multiplayer playlist. // 0 (or omitted) keeps the map out of the regular rotation. MultiplayerFrequency int `json:"multiplayer_frequency"` // Position in the featured grid (1 = first). Featured maps without a // rank sort after ranked ones, alphabetically. FeaturedRank int `json:"featured_rank"` } // hasCategory reports whether the map lists the given category. func (m mapInfo) hasCategory(category string) bool { for _, c := range m.Categories { if c == category { return true } } return false } // generateMapsTS reads every non-test map's info.json and writes the // GameMapType enum, mapCategories, and mapTranslationKeys to // src/core/game/Maps.gen.ts. // Maps appear in registry (alphabetical) order; categories in categoryOrder. func generateMapsTS() error { inputDir, err := inputMapDir(false) if err != nil { return err } cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get working directory: %w", err) } outPath := filepath.Join(cwd, "..", "src", "core", "game", "Maps.gen.ts") infos := make([]mapInfo, 0, len(maps)) for _, m := range maps { if m.IsTest { continue } buf, err := os.ReadFile(filepath.Join(inputDir, m.Name, "info.json")) if err != nil { return fmt.Errorf("failed to read info.json for %s: %w", m.Name, err) } var info mapInfo if err := json.Unmarshal(buf, &info); err != nil { return fmt.Errorf("failed to parse info.json for %s: %w", m.Name, err) } if info.ID == "" || strings.ToLower(info.ID) != m.Name { return fmt.Errorf("map %s: info.json \"id\" (%q) must be the folder name in UpperCamelCase", m.Name, info.ID) } if info.Name == "" { return fmt.Errorf("map %s: info.json is missing \"name\"", m.Name) } if info.TranslationKey == "" { return fmt.Errorf("map %s: info.json is missing \"translation_key\"", m.Name) } if info.MultiplayerFrequency < 0 { return fmt.Errorf("map %s: info.json \"multiplayer_frequency\" (%d) must be >= 0", m.Name, info.MultiplayerFrequency) } if info.FeaturedRank < 0 { return fmt.Errorf("map %s: info.json \"featured_rank\" (%d) must be >= 1", m.Name, info.FeaturedRank) } if info.FeaturedRank > 0 && !info.hasCategory("featured") { return fmt.Errorf("map %s: info.json sets \"featured_rank\" but \"categories\" does not include \"featured\"", m.Name) } if len(info.Categories) == 0 { return fmt.Errorf("map %s: info.json \"categories\" must list at least one category", m.Name) } seen := make(map[string]bool) for _, category := range info.Categories { valid := false for _, c := range categoryOrder { if category == c { valid = true break } } if !valid { return fmt.Errorf("map %s: info.json category %q must be one of: %s", m.Name, category, strings.Join(categoryOrder, ", ")) } if seen[category] { return fmt.Errorf("map %s: info.json lists category %q more than once", m.Name, category) } seen[category] = true } infos = append(infos, info) } var b strings.Builder b.WriteString("// Code generated by map-generator; DO NOT EDIT.\n") b.WriteString("// Map metadata lives in map-generator/assets/maps//info.json.\n") b.WriteString("// Regenerate with `npm run gen-maps`.\n\n") b.WriteString("export enum GameMapType {\n") for _, info := range infos { // Trailing comment so editors can jump straight to the map's info.json. b.WriteString(fmt.Sprintf(" %s = %q, // map-generator/assets/maps/%s/info.json\n", info.ID, info.Name, strings.ToLower(info.ID))) } b.WriteString("}\n\n") b.WriteString("export type GameMapName = keyof typeof GameMapType;\n\n") b.WriteString("export const mapCategories: Record = {\n") for _, category := range categoryOrder { members := make([]mapInfo, 0, len(infos)) for _, info := range infos { if info.hasCategory(category) { members = append(members, info) } } if category == "featured" { sort.SliceStable(members, func(i, j int) bool { ri, rj := members[i].FeaturedRank, members[j].FeaturedRank if ri == 0 { ri = len(infos) + 1 } if rj == 0 { rj = len(infos) + 1 } return ri < rj }) } b.WriteString(fmt.Sprintf(" %s: [\n", category)) for _, info := range members { b.WriteString(fmt.Sprintf(" GameMapType.%s,\n", info.ID)) } b.WriteString(" ],\n") } b.WriteString("};\n\n") b.WriteString("export const mapTranslationKeys: Record = {\n") for _, info := range infos { b.WriteString(fmt.Sprintf(" [GameMapType.%s]: %q,\n", info.ID, info.TranslationKey)) } b.WriteString("};\n\n") b.WriteString("// How many times each map appears in the multiplayer playlist.\n") b.WriteString("export const multiplayerFrequency: Record = {\n") for _, info := range infos { b.WriteString(fmt.Sprintf(" %s: %d,\n", info.ID, info.MultiplayerFrequency)) } b.WriteString("};\n") if err := os.WriteFile(outPath, []byte(b.String()), 0644); err != nil { return fmt.Errorf("failed to write %s: %w", outPath, err) } return nil }