// 世界存档管理器(save.go):level.dat 元数据 + 脏区块写 region + 载入重建。 // // 流程(存档与持久化.md §2–§5): // - 保存:遍历脏区块 → 序列化 → 写 region → 清除 SaveDirty → 写 level.dat; // - 载入:读 level.dat 取 seed → 生成器重建 → 用已保存区块覆盖; // - 全部写盘走临时文件 + 原子替换(崩溃恢复)。 package save import ( "encoding/binary" "encoding/json" "fmt" "os" "path/filepath" "mc/internal/block" "mc/internal/chunk" "mc/internal/coord" "mc/internal/world" ) // LevelData 世界元数据(存档格式.md §2)。 type LevelData struct { Version int `json:"version"` Seed int64 `json:"seed"` Time int64 `json:"time"` GameMode string `json:"game_mode"` } // WriteLevel 写 level.dat(临时文件 + 原子替换)。 func WriteLevel(dir, name string, lv LevelData) error { b, err := json.MarshalIndent(lv, "", " ") if err != nil { return fmt.Errorf("save.WriteLevel: %w", err) } path := filepath.Join(dir, name, "level.dat") tmp, err := os.CreateTemp(filepath.Dir(path), "level.tmp.*") if err != nil { return err } tmpPath := tmp.Name() defer os.Remove(tmpPath) if _, err := tmp.Write(b); err != nil { tmp.Close() return err } if err := tmp.Sync(); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmpPath, path) } // ReadLevel 读 level.dat;不存在返回零值。 func ReadLevel(dir, name string) (LevelData, error) { path := filepath.Join(dir, name, "level.dat") b, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return LevelData{}, nil } return LevelData{}, fmt.Errorf("save.ReadLevel: %w", err) } var lv LevelData if err := json.Unmarshal(b, &lv); err != nil { return LevelData{}, fmt.Errorf("save.ReadLevel: %w", err) } return lv, nil } // EncodeChunk 序列化区块:[魔数 4][版本 1][cx 4][cz 4][方块 65536×4][天空光 32768][方块光 32768]。 func EncodeChunk(c *chunk.Chunk) []byte { blocks := c.Snapshot() sky, bl := c.LightBytes() out := make([]byte, 0, 9+len(blocks)*4+len(sky)+len(bl)) out = append(out, magic...) out = append(out, chunkVer) out = binary.BigEndian.AppendUint32(out, uint32(c.CX)) out = binary.BigEndian.AppendUint32(out, uint32(c.CZ)) for _, s := range blocks { out = binary.BigEndian.AppendUint32(out, uint32(s)) } out = append(out, sky...) out = append(out, bl...) return out } // DecodeChunk 反序列化区块。 func DecodeChunk(data []byte) (*chunk.Chunk, error) { if len(data) < 9 || string(data[0:4]) != magic { return nil, fmt.Errorf("save.DecodeChunk: 魔数错误") } if data[4] != chunkVer { return nil, fmt.Errorf("save.DecodeChunk: 版本 %d 不支持", data[4]) } cx := int32(binary.BigEndian.Uint32(data[5:9])) cz := int32(binary.BigEndian.Uint32(data[9:13])) off := 13 n := chunk.SizeX * chunk.SizeZ * chunk.Height blocks := make([]block.State, n) for i := 0; i < n; i++ { blocks[i] = block.State(binary.BigEndian.Uint32(data[off:])) off += 4 } sky := make([]byte, n/2) bl := make([]byte, n/2) copy(sky, data[off:off+n/2]) off += n / 2 copy(bl, data[off:off+n/2]) c := chunk.New(cx, cz) c.SetAll(blocks, sky, bl) return c, nil } // SaveWorld 保存全部脏区块 + level.dat(存档与持久化.md §2 保存时机由调用方触发)。 func SaveWorld(w *world.World, dir, name string, seed int64, tick int64) error { regionDir := filepath.Join(dir, name, "region") if err := os.MkdirAll(regionDir, 0o755); err != nil { return fmt.Errorf("save.SaveWorld mkdir: %w", err) } for _, c := range w.ActiveChunks() { if !c.SaveDirty.Load() { continue } data := EncodeChunk(c) rx, rz := coord.FloorDiv(c.CX, regionSize), coord.FloorDiv(c.CZ, regionSize) region, err := OpenRegion(regionDir, rx, rz) if err != nil { return err } if err := region.WriteChunk(c.CX, c.CZ, data); err != nil { return err } c.SaveDirty.Store(false) } return WriteLevel(dir, name, LevelData{Version: 1, Seed: seed, Time: tick, GameMode: "survival"}) } // LoadWorld 载入世界:level.dat 取 seed 重建,再用 region 中已保存区块覆盖。 func LoadWorld(w *world.World, dir, name string) (int64, error) { lv, err := ReadLevel(dir, name) if err != nil { return 0, err } regionDir := filepath.Join(dir, name, "region") entries, err := os.ReadDir(regionDir) if err != nil { if os.IsNotExist(err) { return lv.Seed, nil // 新世界 } return 0, fmt.Errorf("save.LoadWorld: %w", err) } for _, e := range entries { if e.IsDir() { continue } var rx, rz int32 if _, err := fmt.Sscanf(e.Name(), "r.%d.%d.mcr", &rx, &rz); err != nil { continue } region, err := OpenRegion(regionDir, rx, rz) if err != nil { return 0, err } for lz := int32(0); lz < regionSize; lz++ { for lx := int32(0); lx < regionSize; lx++ { cx, cz := rx*regionSize+lx, rz*regionSize+lz data, ok, err := region.ReadChunk(cx, cz) if err != nil { return 0, fmt.Errorf("save.LoadWorld %d,%d: %w", cx, cz, err) } if !ok { continue } c, err := DecodeChunk(data) if err != nil { return 0, err } w.InstallChunk(c) } } } return lv.Seed, nil }