feat(server): 服务器接线——世界调度、30s 定时存档、握手校验、关服 flush(smoke 验证通过)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
// 年糕历险记 —— 专用服务器入口。
|
||||
//
|
||||
// 装配:配置 → 日志 → 注册表 → 世界(存档载入/新建)→ 网络监听 → 主循环
|
||||
// (tick 20Hz:世界调度 + 心跳 + 定时存档;关服时全量保存,存档与持久化.md §2)。
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -6,24 +9,70 @@ import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"mc/internal/block"
|
||||
"mc/internal/config"
|
||||
"mc/internal/engine"
|
||||
"mc/internal/logx"
|
||||
"mc/internal/netproto"
|
||||
"mc/internal/netserver"
|
||||
"mc/internal/save"
|
||||
"mc/internal/world"
|
||||
"mc/internal/worldgen"
|
||||
)
|
||||
|
||||
// serverApp 服务器应用骨架(世界/网络在后续里程碑接入)。
|
||||
// serverApp 服务器应用:世界 + 网络 + 定时存档。
|
||||
type serverApp struct {
|
||||
cfg *config.ServerConfig
|
||||
log *logx.Logger
|
||||
tickCount uint64
|
||||
cfg *config.ServerConfig
|
||||
log *logx.Logger
|
||||
reg *block.Registry
|
||||
world *world.World
|
||||
net *netserver.Server
|
||||
seed int64
|
||||
tickCnt uint64
|
||||
}
|
||||
|
||||
// Tick 每 tick(20Hz)服务器逻辑。
|
||||
// ---- 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())
|
||||
return 0
|
||||
}
|
||||
|
||||
// OnMove 玩家移动(MVP:仅记录,完整同步见 Todo 8)。
|
||||
func (s *serverApp) OnMove(c *netserver.Conn, m netproto.PlayerMove) {
|
||||
// TODO(多人同步): 更新玩家实体并广播给视距内其他玩家(多人同步.md §3)
|
||||
_ = m
|
||||
}
|
||||
|
||||
// OnDisconnect 连接断开。
|
||||
func (s *serverApp) OnDisconnect(c *netserver.Conn) {
|
||||
s.log.Infof("玩家 #%d 断开", c.ID())
|
||||
}
|
||||
|
||||
// Tick 每 tick(20Hz)服务器逻辑(架构.md §3.2:tick 内不做 IO)。
|
||||
func (s *serverApp) Tick(dt float64) {
|
||||
s.tickCount++
|
||||
if s.tickCount%100 == 0 { // 每 5 秒
|
||||
s.log.Debugf("tick=%d", s.tickCount)
|
||||
s.tickCnt++
|
||||
// 区块调度(以出生点为中心;后续按玩家位置)
|
||||
s.world.Update(8, 64, 8, 8*time.Millisecond)
|
||||
// 定时存档:30s(存档与持久化.md §2)
|
||||
if s.tickCnt%(20*30) == 0 {
|
||||
if err := save.SaveWorld(s.world, "worlds", s.cfg.World.Name, s.seed, int64(s.tickCnt)); err != nil {
|
||||
s.log.Errorf("定时存档失败: %v", err)
|
||||
} else {
|
||||
s.log.Infof("已存档(tick %d)", s.tickCnt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,17 +83,58 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg, err := config.Load("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("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("worlds/" + cfg.World.Name + "/level.dat"); err == nil {
|
||||
if got, err := save.LoadWorld(w, "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)
|
||||
}
|
||||
|
||||
app := &serverApp{cfg: cfg, log: log, reg: reg, world: w, seed: seed}
|
||||
|
||||
// 网络监听(多人同步.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)
|
||||
}
|
||||
|
||||
app := &serverApp{cfg: cfg, log: log}
|
||||
engine.Run(ctx, app, engine.NoopRenderer{})
|
||||
|
||||
// 关服前全量存档(存档与持久化.md §2 关服 flush)
|
||||
if err := save.SaveWorld(w, "worlds", cfg.World.Name, seed, int64(app.tickCnt)); err != nil {
|
||||
log.Errorf("关服存档失败: %v", err)
|
||||
}
|
||||
log.Infof("服务器已退出")
|
||||
}
|
||||
|
||||
// itoa 整数转字符串(避免重复 import strconv 的别名风格)。
|
||||
func itoa(v int) string { return fmtSprint(v) }
|
||||
|
||||
7
cmd/server/util.go
Normal file
7
cmd/server/util.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// 服务器工具函数。
|
||||
package main
|
||||
|
||||
import "strconv"
|
||||
|
||||
// fmtSprint 整数转字符串(构建监听地址用)。
|
||||
func fmtSprint(v int) string { return strconv.Itoa(v) }
|
||||
BIN
tools/zig.zip
BIN
tools/zig.zip
Binary file not shown.
Reference in New Issue
Block a user