aboutsummaryrefslogtreecommitdiff
path: root/io.go
diff options
context:
space:
mode:
Diffstat (limited to 'io.go')
-rw-r--r--io.go222
1 files changed, 222 insertions, 0 deletions
diff --git a/io.go b/io.go
new file mode 100644
index 0000000..1b2fd5d
--- /dev/null
+++ b/io.go
@@ -0,0 +1,222 @@
+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:
+// ---
+// <yaml metadata>
+// ...
+// <grid lines>
+
+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
+}