初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
## CrashReporter — 全局崩溃/异常日志 Autoload
##
## 职责:
## 1. 捕获 NOTIFICATION_CRASH(引擎级崩溃信号)并写入 user://crash_log.txt
## 2. 捕获 NOTIFICATION_WM_CLOSE_REQUEST(正常退出),记录最后一帧状态
## 3. 游戏启动时检查上次是否有未完成的崩溃日志并上报 EventBus(供 HUD 展示提示)
##
## 注意:此 Autoload 必须排在所有其他 Autoload 之前注册(project.godot [autoload] 顺序),
## 确保其他系统崩溃时本 Reporter 已初始化。
##
## 使用方式:
## CrashReporter.log_error("模块名", "错误描述") → 写入 crash_log.txt(不崩溃)
## CrashReporter.log_fatal("模块名", "致命错误") → 写入后 crash(开发模式下)
extends Node
const LOG_PATH: String = "user://crash_log.txt"
const MAX_LOG_BYTES: int = 512 * 1024 # 512KB 上限,防止无限增长
var _session_start_time: float = 0.0
var _last_wave: int = 0 # 由 WaveManager 每波更新,崩溃时记录当前波次
# ── 生命周期 ────────────────────────────────────────────────────────────────
func _ready() -> void:
_session_start_time = Time.get_unix_time_from_system()
_rotate_log_if_oversized()
_check_previous_crash()
func _notification(what: int) -> void:
match what:
NOTIFICATION_CRASH:
_write_crash_entry("CRASH", "引擎级崩溃(NOTIFICATION_CRASH")
NOTIFICATION_WM_CLOSE_REQUEST:
_write_crash_entry("SHUTDOWN", "正常退出")
get_tree().quit()
# ── 公共接口 ────────────────────────────────────────────────────────────────
## 记录非致命错误(push_error 级别,不终止运行)
func log_error(module: String, message: String) -> void:
push_error("[%s] %s" % [module, message])
_append_log("ERROR", module, message)
## 记录致命错误(开发模式下触发断言失败,发布模式下仅记录)
func log_fatal(module: String, message: String) -> void:
_write_crash_entry("FATAL", "[%s] %s" % [module, message])
assert(false, "[CrashReporter] Fatal: %s%s" % [module, message])
## 由 WaveManager 调用,更新崩溃时记录的波次信息
func set_current_wave(wave: int) -> void:
_last_wave = wave
# ── 内部实现 ─────────────────────────────────────────────────────────────────
func _write_crash_entry(level: String, reason: String) -> void:
var elapsed: float = Time.get_unix_time_from_system() - _session_start_time
var entry: String = (
"\n=== %s @ %s (session +%.1fs, wave=%d) ===\n%s\n" % [
level,
Time.get_datetime_string_from_system(),
elapsed,
_last_wave,
reason
]
)
_append_log_raw(entry)
func _append_log(level: String, module: String, message: String) -> void:
var line: String = "[%s][%s] %s%s\n" % [
Time.get_time_string_from_system(),
level, module, message
]
_append_log_raw(line)
func _append_log_raw(text: String) -> void:
var f := FileAccess.open(LOG_PATH, FileAccess.READ_WRITE)
if f == null:
f = FileAccess.open(LOG_PATH, FileAccess.WRITE)
if f == null:
push_warning("CrashReporter: 无法打开 %s" % LOG_PATH)
return
f.seek_end(0)
f.store_string(text)
func _rotate_log_if_oversized() -> void:
if not FileAccess.file_exists(LOG_PATH):
return
if FileAccess.get_file_as_bytes(LOG_PATH).size() > MAX_LOG_BYTES:
var archived: String = LOG_PATH.replace(".txt", "_prev.txt")
DirAccess.rename_absolute(
ProjectSettings.globalize_path(LOG_PATH),
ProjectSettings.globalize_path(archived)
)
func _check_previous_crash() -> void:
## 检查上次是否有 CRASH 或 FATAL 记录,若有则在下次启动时通过 EventBus 通知
if not FileAccess.file_exists(LOG_PATH):
return
var content: String = FileAccess.get_file_as_string(LOG_PATH)
if "=== CRASH" in content or "=== FATAL" in content:
# 延迟到第一帧,确保 EventBus 已初始化
call_deferred("_emit_crash_detected")
func _emit_crash_detected() -> void:
if Engine.has_singleton("EventBus"):
# EventID 暂用 0(特殊保留 ID),HUD 订阅后展示"上次游戏异常退出"提示
EventBus.emit(0, {"source": "crash_reporter", "log_path": LOG_PATH})