Files

228 lines
6.8 KiB
Go
Raw Permalink Normal View History

// 世界存档管理器(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"
2026-09-19 14:21:08 +08:00
"mc/internal/blockentity"
"mc/internal/chunk"
"mc/internal/world"
)
2026-09-19 14:21:08 +08:00
// 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 {
2026-09-19 14:21:08 +08:00
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")
2026-09-19 14:21:08 +08:00
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
}
2026-09-19 14:21:08 +08:00
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 {
2026-09-19 14:21:08 +08:00
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)
}
2026-09-19 14:21:08 +08:00
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)
2026-09-19 14:21:08 +08:00
c.SaveDirty.Store(false)
if pool != nil {
pool.Enqueue(ChunkJob{RegionDir: regionDir, CX: c.CX, CZ: c.CZ, Data: data})
continue
}
2026-09-19 14:21:08 +08:00
if err := writeChunkBytes(regionDir, c.CX, c.CZ, data); err != nil {
return err
}
}
2026-09-19 14:21:08 +08:00
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 {
2026-09-19 14:21:08 +08:00
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)
2026-09-19 14:21:08 +08:00
if err != nil || !ok {
continue // 损坏记录视为未保存(存档格式.md §5)
}
c, err := DecodeChunk(data)
if err != nil {
2026-09-19 14:21:08 +08:00
continue
}
w.InstallChunk(c)
}
}
}
return lv.Seed, nil
}