228 lines
6.8 KiB
Go
228 lines
6.8 KiB
Go
// 世界存档管理器(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
|
||
}
|