fix(crash): 修复双击闪退——路径基于可执行文件目录解析、崩溃兜底日志、资源入 bin 与安装包

This commit is contained in:
NianGao Dev
2026-08-16 06:31:00 +08:00
parent 157c769473
commit 854440216f
7 changed files with 92 additions and 18 deletions

View File

@@ -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")

View File

@@ -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("服务器已退出")

Binary file not shown.

27
internal/crash/crash.go Normal file
View File

@@ -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)
}
}

33
internal/paths/paths.go Normal file
View File

@@ -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...)
}

View File

@@ -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(含资源) =="

View File

@@ -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 中文显示)