Files
ngzz-mc/cmd/server/main.go
2026-09-19 14:21:08 +08:00

199 lines
6.8 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.
// 年糕历险记 —— 专用服务器入口。
//
// 装配:配置 → 日志 → 注册表 → 世界(存档载入/新建)→ 网络监听 → 主循环
// 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 每 tick20Hz服务器逻辑架构.md §3.2tick 内不做 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_tickMod开发指南.md §4.3
if s.tickCnt%20 == 0 {
for _, m := range s.luaMods {
_ = m.DispatchEvent("tick")
}
}
// 定时存档30s 入队(存档与持久化.md §4tick 内不 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种子 %dtick 率 %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("已载入世界 %qseed %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) }