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