package main import ( "bytes" "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // Save format is YAML header with metadata, then the text grid below. // Format: // --- // // ... // type SaveData struct { Name string `yaml:"name"` Width int `yaml:"width"` Height int `yaml:"height"` Palette []Terrain `yaml:"palette"` GridBody string `yaml:"grid_body,omitempty"` GridColors []string `yaml:"grid_colors,omitempty"` TextLabels []TextLabel `yaml:"text_labels,omitempty"` Submaps []SubmapRef `yaml:"submaps,omitempty"` } type SubmapRef struct { X int `yaml:"x"` Y int `yaml:"y"` Data SaveData `yaml:"data"` } // SerializeMap serializes the map to the save format. func SerializeMap(m *Map, path string) error { data := buildSaveData(m) yamlBytes, err := yaml.Marshal(data) if err != nil { return err } var buf bytes.Buffer buf.WriteString("---\n") buf.Write(yamlBytes) buf.WriteString("...\n") for _, row := range m.Grid { for _, cell := range row { if cell.Terrain < 0 || cell.Terrain >= len(m.Palette) { buf.WriteByte(' ') } else { buf.WriteString(m.Palette[cell.Terrain].ASCII) } } buf.WriteByte('\n') } dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return err } return os.WriteFile(path, buf.Bytes(), 0644) } func buildSaveData(m *Map) SaveData { sd := SaveData{ Name: m.Name, Width: m.Width, Height: m.Height, Palette: m.Palette, } // Build grid body (only stored in YAML for submaps) var gb strings.Builder for _, row := range m.Grid { for _, cell := range row { if cell.Terrain < 0 || cell.Terrain >= len(m.Palette) { gb.WriteByte(' ') } else { gb.WriteString(m.Palette[cell.Terrain].ASCII) } } gb.WriteByte('\n') } if m.Parent != nil { sd.GridBody = gb.String() } for y := range m.Grid { for x := range m.Grid[y] { c := m.Grid[y][x] if c.Color != "" { sd.GridColors = append(sd.GridColors, fmt.Sprintf("%d,%d,%s", x, y, c.Color)) } } } for _, tl := range m.TextLabels { sd.TextLabels = append(sd.TextLabels, tl) } for pt, sub := range m.Submaps { sd.Submaps = append(sd.Submaps, SubmapRef{ X: pt.X, Y: pt.Y, Data: buildSaveData(sub), }) } return sd } // DeserializeMap reads a saved map file. func DeserializeMap(path string) (*Map, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } content := string(data) if !strings.HasPrefix(content, "---\n") { return nil, fmt.Errorf("invalid save file: missing YAML header") } endIdx := strings.Index(content, "\n...\n") if endIdx < 0 { return nil, fmt.Errorf("invalid save file: missing ... terminator") } yamlPart := content[4:endIdx] gridPart := content[endIdx+5:] var sd SaveData if err := yaml.Unmarshal([]byte(yamlPart), &sd); err != nil { return nil, fmt.Errorf("invalid YAML header: %w", err) } m := NewMap(sd.Name, sd.Width, sd.Height, sd.Palette) m.Filename = path // Restore colors for _, entry := range sd.GridColors { var x, y int var c string if n, _ := fmt.Sscanf(entry, "%d,%d,%s", &x, &y, &c); n == 3 { if y >= 0 && y < m.Height && x >= 0 && x < m.Width { m.Grid[y][x].Color = c } } } lines := strings.Split(strings.TrimRight(gridPart, "\n"), "\n") for y, line := range lines { if y >= m.Height { break } for x, ch := range line { if x >= m.Width { break } for i, t := range m.Palette { if string(ch) == t.ASCII { m.Grid[y][x].Terrain = i break } } } } for _, tl := range sd.TextLabels { PlaceTextLabel(m, tl.Start, tl.Text, tl.Color) } for _, sr := range sd.Submaps { sub := restoreMap(&sr.Data, m) m.Submaps[Point{sr.X, sr.Y}] = sub } return m, nil } func restoreMap(sd *SaveData, parent *Map) *Map { m := NewMap(sd.Name, sd.Width, sd.Height, sd.Palette) m.Parent = parent // Restore colors for _, entry := range sd.GridColors { var x, y int var c string if n, _ := fmt.Sscanf(entry, "%d,%d,%s", &x, &y, &c); n == 3 { if y >= 0 && y < m.Height && x >= 0 && x < m.Width { m.Grid[y][x].Color = c } } } // Restore grid from GridBody if sd.GridBody != "" { lines := strings.Split(strings.TrimRight(sd.GridBody, "\n"), "\n") for y, line := range lines { if y >= m.Height { break } for x, ch := range line { if x >= m.Width { break } for i, t := range m.Palette { if string(ch) == t.ASCII { m.Grid[y][x].Terrain = i break } } } } } for _, tl := range sd.TextLabels { PlaceTextLabel(m, tl.Start, tl.Text, tl.Color) } for _, sr := range sd.Submaps { sub := restoreMap(&sr.Data, m) m.Submaps[Point{sr.X, sr.Y}] = sub } return m }