Files
spellforge/scripts/autoloads/crash_reporter.gd
T
2026-07-20 10:56:52 +08:00

109 lines
4.4 KiB
GDScript
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.
## 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})