// 年糕历险记 —— 专用服务器入口。 // // 装配:配置 → 日志 → 注册表 → 世界(存档载入/新建)→ 网络监听 → 主循环 // (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) }