mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:10:42 +00:00
3de5fb4204
**Add approved & assigned issue number here:** N/A — maintainer refactor. ## Description: Makes each map's `info.json` the single source of truth for map metadata — adding a map is now a folder with `image.png` + `info.json`, a `gen-maps` run, and an en.json display name. **info.json / manifest.json carry full map metadata.** Every `map-generator/assets/maps/<map>/info.json` declares `id` (the `GameMapType` enum key), `name` (the enum value — wire format, unchanged for all 94 maps), `translation_key`, `categories`, and `multiplayer_frequency` (the public-playlist weight that used to be the `FREQUENCY` record in MapPlaylist.ts). The generator validates everything and mirrors it into `resources/maps/<map>/manifest.json`. 23 stale info.json `name` values were normalized to the canonical enum value; enum values are byte-identical, so replays and stored game configs are unaffected. **The generator emits the TypeScript and discovers maps itself.** New `map-generator/codegen.go` generates `src/core/game/Maps.gen.ts` (`GameMapType`, `GameMapName`, `mapCategories`, `mapTranslationKeys`, `multiplayerFrequency` — now a full `Record<GameMapName, number>`, killing the old `Partial`) on every run; `Game.ts` re-exports it. The hardcoded map registry in `main.go` is gone — maps are auto-discovered from the `assets/maps` / `assets/test_maps` directories. MapConsistency tests fail with a "run `npm run gen-maps`" message if info.json, manifest.json, and Maps.gen.ts drift. The tracked `map-generator/map-generator` binary is rebuilt to match. **New categories: continents + world/cosmic/tournament/other, multi-category support.** `continental`/`regional`/`fantasy`/`arcade` are replaced by `featured`, `world`, `europe`, `asia`, `north_america`, `africa`, `south_america`, `oceania`, `antarctica`, `cosmic`, `tournament`, and `other`. Maps can list multiple categories, so straddlers (Black Sea, Bosphorus, Caucasus, Between Two Seas, Bering Sea/Strait, Mena, Strait of Gibraltar, Hawaii, Arctic) appear under both regions. Featured is itself a category (same 7 maps as before). MapPlaylist keeps its arcade exclusion via an explicit set. **Map picker UI.** Two tabs: **Featured** (default — featured maps plus a Favorites section when maps are starred) and **All** (one prominent collapsible bar per category with a map count, collapsed by default). The selected map is prepended to the featured grid when it lives elsewhere. `getMapName()` resolves through the generated `mapTranslationKeys`, which also fixes tourney maps never resolving a valid translation key. ## Please complete the following: - [ ] I have added screenshots for all UI updates (maintainer change — picker described above) - [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 ## Please put your Discord username so you can be contacted if a bug or regression is found: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
183 lines
5.7 KiB
Go
183 lines
5.7 KiB
Go
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/<map>/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<string, GameMapType[]> = {\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<GameMapType, string> = {\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<GameMapName, number> = {\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
|
|
}
|