// 世界存档管理器(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/blockentity" "mc/internal/chunk" "mc/internal/world" ) // InvSlot 背包槽快照(存档格式.md §2 player.slots)。 type InvSlot struct { Name string `json:"name"` Count uint8 `json:"count"` Durability int32 `json:"durability,omitempty"` } // PlayerData 玩家快照(存档格式.md §2)。 type PlayerData struct { X, Y, Z float64 `json:"x"` Yaw, Pitch float64 `json:"yaw"` Health float64 `json:"health"` Hunger int `json:"hunger"` Saturation float64 `json:"saturation,omitempty"` Selected int `json:"selected"` Slots []InvSlot `json:"slots,omitempty"` UnlockedRecipes []string `json:"unlocked_recipes,omitempty"` } // LevelData 世界元数据(存档格式.md §2)。 type LevelData struct { Version int `json:"version"` Seed int64 `json:"seed"` Time int64 `json:"time"` GameMode string `json:"game_mode"` Spawn [3]float64 `json:"spawn,omitempty"` Player *PlayerData `json:"player,omitempty"` BlockEntities []blockentity.Snapshot `json:"block_entities,omitempty"` } // 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") if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("save.WriteLevel: %w", err) } 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 atomicReplace(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 { return SaveWorldLevel(w, dir, name, LevelData{Version: 1, Seed: seed, Time: tick, GameMode: "survival"}) } // SaveWorldLevel 保存脏区块并写入完整 level.dat(可带玩家快照)。 func SaveWorldLevel(w *world.World, dir, name string, lv LevelData) error { regionDir := filepath.Join(dir, name, "region") if err := os.MkdirAll(regionDir, 0o755); err != nil { return fmt.Errorf("save.SaveWorld mkdir: %w", err) } if err := EnqueueDirty(w, nil, regionDir); err != nil { return err } if lv.Version == 0 { lv.Version = 1 } return WriteLevel(dir, name, lv) } // EnqueueDirty 编码脏区块:pool != nil 则异步入队,否则同步写 region。 func EnqueueDirty(w *world.World, pool *SavePool, regionDir string) error { if err := os.MkdirAll(regionDir, 0o755); err != nil { return fmt.Errorf("save.EnqueueDirty mkdir: %w", err) } for _, c := range w.ActiveChunks() { if !c.SaveDirty.Load() { continue } data := EncodeChunk(c) c.SaveDirty.Store(false) if pool != nil { pool.Enqueue(ChunkJob{RegionDir: regionDir, CX: c.CX, CZ: c.CZ, Data: data}) continue } if err := writeChunkBytes(regionDir, c.CX, c.CZ, data); err != nil { return err } } return nil } // 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 { continue // 单个 region 打不开:跳过,不中止整世界 } 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 || !ok { continue // 损坏记录视为未保存(存档格式.md §5) } c, err := DecodeChunk(data) if err != nil { continue } w.InstallChunk(c) } } } return lv.Seed, nil }