diff --git a/cmd/client/client_gl.go b/cmd/client/client_gl.go index 60c6e831..b6a553f7 100644 --- a/cmd/client/client_gl.go +++ b/cmd/client/client_gl.go @@ -19,6 +19,7 @@ import ( "mc/internal/block" "mc/internal/camera" "mc/internal/config" + "mc/internal/crash" "mc/internal/engine" "mc/internal/font" "mc/internal/game" @@ -28,6 +29,7 @@ import ( "mc/internal/mesh" "mc/internal/netclient" "mc/internal/netproto" + "mc/internal/paths" "mc/internal/physics" "mc/internal/recipe" "mc/internal/render" @@ -149,7 +151,7 @@ func (a *clientApp) Tick(dt float64) { // 定时存档:30s(存档与持久化.md §2) a.tickCnt++ if a.tickCnt%(20*30) == 0 && a.net == nil { - if err := save.SaveWorld(a.sess.World, "worlds", "NewWorld", 123456789, int64(a.tickCnt)); err != nil { + if err := save.SaveWorld(a.sess.World, paths.Join("worlds"), "NewWorld", 123456789, int64(a.tickCnt)); err != nil { // 日志由调用方输出(避免每次 tick 刷屏) _ = err } @@ -221,13 +223,14 @@ func (a *clientApp) logf(format string, args ...any) { } func main() { + defer crash.Guard(paths.Join("logs", "crash.log")) // 崩溃兜底:闪退可诊断 flag.Parse() - log, err := logx.New("logs/client.log", logx.LevelInfo) + log, err := logx.New(paths.Join("logs", "client.log"), logx.LevelInfo) if err != nil { panic(err) } - cfg, err := config.Load("config/client.yaml", config.DefaultClient()) + cfg, err := config.Load(paths.Join("config", "client.yaml"), config.DefaultClient()) if err != nil { log.Warnf("配置加载失败,使用默认值:%v", err) } @@ -235,7 +238,7 @@ func main() { // 1) 内容包与图集(设计.md §1.3:扫描 mods/ + resourcepacks/) am := assets.New() - if err := am.Load("assets", "mods", "resourcepacks"); err != nil { + if err := am.Load(paths.Join("assets"), paths.Join("mods"), paths.Join("resourcepacks")); err != nil { log.Warnf("内容包加载失败:%v", err) } log.Infof("已加载 %d 个内容包", len(am.Packs())) @@ -282,15 +285,15 @@ func main() { } // 2) 注册表(设计.md §6.1/§6.2 数据驱动) - reg, err := block.Load("assets/config/blocks.json") + reg, err := block.Load(paths.Join("assets", "config", "blocks.json")) if err != nil { panic(err) } - items, err := item.Load("assets/config/items.json") + items, err := item.Load(paths.Join("assets", "config", "items.json")) if err != nil { panic(err) } - recipes, err := recipe.Load("assets/config/recipes.json") + recipes, err := recipe.Load(paths.Join("assets", "config", "recipes.json")) if err != nil { panic(err) } @@ -307,8 +310,8 @@ func main() { defer w.Close() // 单机存档(存档与持久化.md §3:diff-only 载入,无存档则新建) - if _, err := os.Stat("worlds/NewWorld/level.dat"); err == nil { - if _, err := save.LoadWorld(w, "worlds", "NewWorld"); err != nil { + if _, err := os.Stat(paths.Join("worlds", "NewWorld", "level.dat")); err == nil { + if _, err := save.LoadWorld(w, paths.Join("worlds"), "NewWorld"); err != nil { log.Warnf("存档载入失败: %v", err) } else { log.Infof("已载入世界 NewWorld") diff --git a/cmd/server/main.go b/cmd/server/main.go index 4daa23b5..23377a5b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,12 +14,14 @@ import ( "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" @@ -82,7 +84,7 @@ func (s *serverApp) Tick(dt float64) { } // 定时存档: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 { + if err := save.SaveWorld(s.world, paths.Join("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) @@ -91,20 +93,21 @@ func (s *serverApp) Tick(dt float64) { } func main() { + defer crash.Guard(paths.Join("logs", "crash.log")) // 崩溃兜底:闪退可诊断 flag.Parse() - log, err := logx.New("logs/server.log", logx.LevelInfo) + log, err := logx.New(paths.Join("logs", "server.log"), logx.LevelInfo) if err != nil { panic(err) } - cfg, err := config.Load("config/server.yaml", config.DefaultServer()) + 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("assets/config/blocks.json") + reg, err := block.Load(paths.Join("assets", "config", "blocks.json")) if err != nil { panic(err) } @@ -120,8 +123,8 @@ func main() { // 载入已有存档(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 { + 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 @@ -136,7 +139,7 @@ func main() { // Mod 加载(设计.md §3.3:扫描 mods/,type=mod 且有入口脚本的执行;热更新.md 由注册表支持) am := assets.New() - if err := am.Load("assets", "mods", "resourcepacks"); err != nil { + if err := am.Load(paths.Join("assets"), paths.Join("mods"), paths.Join("resourcepacks")); err != nil { log.Warnf("内容包扫描失败: %v", err) } app.modReg = mods.NewRegistry() @@ -169,7 +172,7 @@ func main() { engine.Run(ctx, app, engine.NoopRenderer{}) // 关服前全量存档(存档与持久化.md §2 关服 flush) - if err := save.SaveWorld(w, "worlds", cfg.World.Name, seed, int64(app.tickCnt)); err != nil { + if err := save.SaveWorld(w, paths.Join("worlds"), cfg.World.Name, seed, int64(app.tickCnt)); err != nil { log.Errorf("关服存档失败: %v", err) } log.Infof("服务器已退出") diff --git a/dist/年糕历险记-Setup-v0.1.0.zip b/dist/年糕历险记-Setup-v0.1.0.zip index 6a08f63a..2255a904 100644 Binary files a/dist/年糕历险记-Setup-v0.1.0.zip and b/dist/年糕历险记-Setup-v0.1.0.zip differ diff --git a/internal/crash/crash.go b/internal/crash/crash.go new file mode 100644 index 00000000..54a37c12 --- /dev/null +++ b/internal/crash/crash.go @@ -0,0 +1,27 @@ +// Package crash 崩溃兜底:panic 时把堆栈写入崩溃日志(闪退可诊断)。 +package crash + +import ( + "fmt" + "os" + "path/filepath" + "runtime/debug" + "time" +) + +// Guard 在 main 顶部 defer Guard();panic 时写 crash.log 后重抛(保留退出行为)。 +func Guard(logPath string) { + if r := recover(); r != nil { + msg := fmt.Sprintf("[%s] 崩溃: %v\n%s\n", time.Now().Format("2006-01-02 15:04:05"), r, debug.Stack()) + if logPath != "" { + _ = os.MkdirAll(filepath.Dir(logPath), 0o755) + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + if err == nil { + _, _ = f.WriteString(msg) + _ = f.Close() + } + } + _, _ = fmt.Fprint(os.Stderr, msg) + panic(r) // 保留原始 panic 行为(退出码非 0) + } +} diff --git a/internal/paths/paths.go b/internal/paths/paths.go new file mode 100644 index 00000000..927c9eab --- /dev/null +++ b/internal/paths/paths.go @@ -0,0 +1,33 @@ +// Package paths 资源路径解析:全部相对路径基于「可执行文件所在目录」解析, +// 保证双击运行 / 桌面快捷方式 / 安装目录下任意 CWD 都能找到资源(修复闪退根因)。 +package paths + +import ( + "os" + "path/filepath" + "sync" +) + +var ( + once sync.Once + baseDir string +) + +// BaseDir 可执行文件所在目录(缓存;失败回退 CWD)。 +func BaseDir() string { + once.Do(func() { + exe, err := os.Executable() + if err != nil { + baseDir, _ = os.Getwd() + return + } + baseDir = filepath.Dir(exe) + }) + return baseDir +} + +// Join 基于可执行文件目录拼接相对路径。 +func Join(rel ...string) string { + parts := append([]string{BaseDir()}, rel...) + return filepath.Join(parts...) +} diff --git a/scripts/build.ps1 b/scripts/build.ps1 index b79ac03f..57151e96 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -37,7 +37,13 @@ try { New-Item -ItemType Directory -Force -Path bin | Out-Null go build -tags $tags -trimpath -ldflags "-s -w" -o bin/mc-client.exe ./cmd/client go build -trimpath -ldflags "-s -w" -o bin/mc-server.exe ./cmd/server + + # 资源入 bin/(可执行文件目录解析:双击/快捷方式运行无需依赖工作目录) + Copy-Item (Join-Path $root "config") (Join-Path $root "bin") -Recurse -Force + Copy-Item (Join-Path $root "assets") (Join-Path $root "bin") -Recurse -Force + Copy-Item (Join-Path $root "mods") (Join-Path $root "bin") -Recurse -Force + Copy-Item (Join-Path $root "resourcepacks") (Join-Path $root "bin") -Recurse -Force } finally { Pop-Location } -Write-Host "== 构建完成:bin/mc-client.exe bin/mc-server.exe ==" +Write-Host "== 构建完成:bin/mc-client.exe bin/mc-server.exe(含资源) ==" diff --git a/scripts/make_installer.ps1 b/scripts/make_installer.ps1 index 58f9a9a4..096e9c81 100644 --- a/scripts/make_installer.ps1 +++ b/scripts/make_installer.ps1 @@ -22,6 +22,8 @@ Copy-Item (Join-Path $root "config\client.yaml") (Join-Path $stage "config") Copy-Item (Join-Path $root "config\server.yaml") (Join-Path $stage "config") New-Item -ItemType Directory -Force -Path (Join-Path $stage "mods") | Out-Null Copy-Item (Join-Path $root "mods\README.md") (Join-Path $stage "mods") +# 游戏数据(方块/物品/配方注册表) +Copy-Item (Join-Path $root "assets") (Join-Path $stage "assets") -Recurse # 默认素材包 Copy-Item (Join-Path $root "resourcepacks") (Join-Path $stage "resourcepacks") -Recurse # 安装脚本(GBK 编码保证 cmd 中文显示)