Files
ngzz-mc/internal/save/save.go

188 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 世界存档管理器(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
}