199 lines
6.8 KiB
Go
199 lines
6.8 KiB
Go
// 年糕历险记 —— 专用服务器入口。
|
||
//
|
||
// 装配:配置 → 日志 → 注册表 → 世界(存档载入/新建)→ 网络监听 → 主循环
|
||
// (tick 20Hz:世界调度 + 心跳 + 定时存档;关服时全量保存,存档与持久化.md §2)。
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"flag"
|
||
"os"
|
||
"os/signal"
|
||
"time"
|
||
|
||
"mc/internal/assets"
|
||
"mc/internal/block"
|
||
"mc/internal/config"
|
||
"mc/internal/crash"
|
||
"mc/internal/engine"
|
||
"mc/internal/logx"
|
||
"mc/internal/mods"
|
||
"mc/internal/mpsync"
|
||
"mc/internal/netproto"
|
||
"mc/internal/netserver"
|
||
"mc/internal/paths"
|
||
"mc/internal/save"
|
||
"mc/internal/world"
|
||
"mc/internal/worldgen"
|
||
)
|
||
|
||
// serverApp 服务器应用:世界 + 网络 + 定时存档 + Mod。
|
||
type serverApp struct {
|
||
cfg *config.ServerConfig
|
||
log *logx.Logger
|
||
reg *block.Registry
|
||
world *world.World
|
||
net *netserver.Server
|
||
hub *mpsync.Hub
|
||
modReg *mods.Registry
|
||
luaMods []*mods.LuaMod
|
||
seed int64
|
||
tickCnt uint64
|
||
savePool *save.SavePool
|
||
}
|
||
|
||
// ---- netserver.Handler 实现(多人同步.md §2 权威链路入口)----
|
||
|
||
// OnConnect 连接建立。
|
||
func (s *serverApp) OnConnect(c *netserver.Conn) {
|
||
s.log.Infof("玩家连接 #%d", c.ID())
|
||
}
|
||
|
||
// OnHandshake 握手校验(网络协议.md §4:协议版本不匹配拒绝)+ 加入同步中心。
|
||
func (s *serverApp) OnHandshake(c *netserver.Conn, hs netproto.Handshake) uint8 {
|
||
if hs.ProtocolVersion != 1 {
|
||
s.log.Warnf("玩家 #%d 协议版本 %d 不匹配,拒绝", c.ID(), hs.ProtocolVersion)
|
||
return 1
|
||
}
|
||
s.log.Infof("玩家 %q 登录(连接 #%d)", hs.ClientID, c.ID())
|
||
// 登录成功:加入广播集并发送区块(多人同步.md §4)
|
||
s.hub.AddPlayer(c, 8, 8)
|
||
return 0
|
||
}
|
||
|
||
// OnMove 玩家移动:广播给其他客户端(多人同步.md §3)。
|
||
func (s *serverApp) OnMove(c *netserver.Conn, m netproto.PlayerMove) {
|
||
s.hub.BroadcastEntityMove(c, uint32(c.ID()), m)
|
||
}
|
||
|
||
// OnDisconnect 连接断开:移出同步中心。
|
||
func (s *serverApp) OnDisconnect(c *netserver.Conn) {
|
||
s.log.Infof("玩家 #%d 断开", c.ID())
|
||
s.hub.RemovePlayer(c)
|
||
}
|
||
|
||
// Tick 每 tick(20Hz)服务器逻辑(架构.md §3.2:tick 内不做 IO)。
|
||
func (s *serverApp) Tick(dt float64) {
|
||
s.tickCnt++
|
||
// 区块调度(以出生点为中心;后续按玩家位置)
|
||
s.world.Update(8, 64, 8, 8*time.Millisecond)
|
||
regionDir := paths.Join("worlds", s.cfg.World.Name, "region")
|
||
for _, c := range s.world.DrainUnloaded() {
|
||
if c.SaveDirty.Load() && s.savePool != nil {
|
||
s.savePool.Enqueue(save.ChunkJob{RegionDir: regionDir, CX: c.CX, CZ: c.CZ, Data: save.EncodeChunk(c)})
|
||
c.SaveDirty.Store(false)
|
||
}
|
||
}
|
||
// Mod 事件:每秒派发 on_tick(Mod开发指南.md §4.3)
|
||
if s.tickCnt%20 == 0 {
|
||
for _, m := range s.luaMods {
|
||
_ = m.DispatchEvent("tick")
|
||
}
|
||
}
|
||
// 定时存档:30s 入队(存档与持久化.md §4:tick 内不 fsync)
|
||
if s.tickCnt%(20*30) == 0 {
|
||
if err := save.EnqueueDirty(s.world, s.savePool, regionDir); err != nil {
|
||
s.log.Errorf("定时存档失败: %v", err)
|
||
} else if err := save.WriteLevel(paths.Join("worlds"), s.cfg.World.Name, save.LevelData{Version: 1, Seed: s.seed, Time: int64(s.tickCnt)}); err != nil {
|
||
s.log.Errorf("定时存档失败: %v", err)
|
||
} else {
|
||
s.log.Infof("已存档(tick %d)", s.tickCnt)
|
||
}
|
||
}
|
||
}
|
||
|
||
func main() {
|
||
crash.SetLogPath(paths.Join("logs", "crash.log")) // 全局崩溃日志(含 goroutine 兜底)
|
||
defer crash.Guard(paths.Join("logs", "crash.log")) // 主线程崩溃兜底:闪退可诊断
|
||
flag.Parse()
|
||
|
||
log, err := logx.New(paths.Join("logs", "server.log"), logx.LevelInfo)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
cfg, err := config.Load(paths.Join("config", "server.yaml"), config.DefaultServer())
|
||
if err != nil {
|
||
log.Warnf("配置加载失败,使用默认值:%v", err)
|
||
}
|
||
log.Infof("年糕历险记 服务器启动:端口 %d,世界 %q,种子 %d,tick 率 %d", cfg.Server.Port, cfg.World.Name, cfg.World.Seed, cfg.Server.TickRate)
|
||
|
||
// 注册表与生成器
|
||
reg, err := block.Load(paths.Join("assets", "config", "blocks.json"))
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
gen, err := worldgen.New(reg, cfg.World.Seed, cfg.World.Generator.BiomeScale, cfg.World.Generator.HeightScale, cfg.World.Generator.CaveDensity)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
w, err := world.New(reg, gen, cfg.Server.ViewDistance)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
defer w.Close()
|
||
|
||
// 载入已有存档(diff-only:生成器重建 + 已保存区块覆盖,存档与持久化.md §3)
|
||
seed := cfg.World.Seed
|
||
if _, err := os.Stat(paths.Join("worlds", cfg.World.Name, "level.dat")); err == nil {
|
||
if got, err := save.LoadWorld(w, paths.Join("worlds"), cfg.World.Name); err != nil {
|
||
log.Errorf("存档载入失败: %v", err)
|
||
} else if got != 0 {
|
||
seed = got
|
||
log.Infof("已载入世界 %q(seed %d)", cfg.World.Name, seed)
|
||
}
|
||
} else {
|
||
log.Infof("新建世界 %q", cfg.World.Name)
|
||
}
|
||
|
||
pool := save.NewSavePool(log)
|
||
app := &serverApp{cfg: cfg, log: log, reg: reg, world: w, seed: seed, savePool: pool}
|
||
app.hub = mpsync.NewHub(w, cfg.Server.ViewDistance)
|
||
|
||
// Mod 加载(设计.md §3.3:扫描 mods/,type=mod 且有入口脚本的执行;热更新.md 由注册表支持)
|
||
am := assets.New()
|
||
if err := am.Load(paths.Join("assets"), paths.Join("mods"), paths.Join("resourcepacks")); err != nil {
|
||
log.Warnf("内容包扫描失败: %v", err)
|
||
}
|
||
app.modReg = mods.NewRegistry()
|
||
for _, p := range am.Packs() {
|
||
if p.Type != assets.PackMod || p.Manifest == nil || p.Manifest.Entry == "" {
|
||
continue
|
||
}
|
||
script, err := os.ReadFile(p.Root + "/" + p.Manifest.Entry)
|
||
if err != nil {
|
||
log.Warnf("Mod %q 入口脚本读取失败: %v", p.ID, err)
|
||
continue
|
||
}
|
||
m := mods.NewLuaMod(p.ID, app.modReg)
|
||
if err := m.Load(string(script)); err != nil {
|
||
log.Warnf("Mod %q 加载失败: %v", p.ID, err)
|
||
continue
|
||
}
|
||
app.luaMods = append(app.luaMods, m)
|
||
log.Infof("已加载 Mod %q(注册 %d 条)", p.ID, len(app.modReg.Entries()))
|
||
}
|
||
|
||
// 网络监听(多人同步.md §7:心跳由连接层自动处理)
|
||
app.net = netserver.New("0.0.0.0:"+itoa(cfg.Server.Port), log, app)
|
||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||
defer stop()
|
||
if err := app.net.Listen(ctx); err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
engine.Run(ctx, app, engine.NoopRenderer{})
|
||
|
||
// 关服前全量存档 + drain 保存池(存档与持久化.md §4)
|
||
if err := save.EnqueueDirty(w, pool, paths.Join("worlds", cfg.World.Name, "region")); err != nil {
|
||
log.Errorf("关服存档失败: %v", err)
|
||
}
|
||
if err := save.WriteLevel(paths.Join("worlds"), cfg.World.Name, save.LevelData{Version: 1, Seed: seed, Time: int64(app.tickCnt)}); err != nil {
|
||
log.Errorf("关服存档失败: %v", err)
|
||
}
|
||
pool.Close()
|
||
log.Infof("服务器已退出")
|
||
}
|
||
|
||
// itoa 整数转字符串(避免重复 import strconv 的别名风格)。
|
||
func itoa(v int) string { return fmtSprint(v) }
|