From b8e473b069d9c81e2f6a471fa7636c27cee63fae Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 23 Jun 2026 04:38:06 -0400 Subject: initial commit --- io.go | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 io.go (limited to 'io.go') 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: +// --- +// +// ... +// + +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 +} -- cgit v1.2.3