# VERTICAL SLICE - NOT FOR PRODUCTION # 探针共用面:树寻址助手 + 存档/设置/元进度隔离样板单点。 # 覆盖:ui_probe / ui_click_probe / shot_probe / fx_shot_probe 四探针的树寻址样板 # 与隔离样板(各探针隔离文件名后缀 tag 保持不变)。 # 用法:探针侧 `const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")` 后静态调用 #(探针载体含 --script 场景两种,均无全局标识符——禁 class_name 裸引用)。 extends RefCounted const SaveManagerScript := preload("res://core/save_manager.gd") const MetaProgressionScript := preload("res://core/meta_progression.gd") const SettingsStoreScript := preload("res://presentation/settings_store.gd") # ── 树寻址(递归只读——按钮/标签句柄未存组合根成员的探针专用面) ────────────── # 匹配模式参数化(历史两种语义,禁混用禁静默改变): # mode="prefix" = text.begins_with(needle)(ui_probe / ui_click_probe 历史语义) # mode="contains" = text.contains(needle)(shot_probe / fx_shot_probe 历史语义) # 调用点替换时逐一核对原语义后选参。 static func find_button(root: Node, needle: String, mode: String = "prefix") -> Button: if root == null: return null if root is Button and _text_match(String(root.text), needle, mode): return root for child in root.get_children(): var found := find_button(child, needle, mode) if found != null: return found return null # CheckButton 同款寻址(ui_click_probe 语义 = contains——断言设置面旧入口不在) static func find_check_button(root: Node, needle: String) -> CheckButton: if root == null: return null if root is CheckButton and String(root.text).contains(needle): return root for child in root.get_children(): var found := find_check_button(child, needle) if found != null: return found return null # Label 按文案包含寻址(shot_probe GP 徽章读数锚) static func find_label(root: Node, text_fragment: String) -> Label: if root == null: return null if root is Label and String(root.text).contains(text_fragment): return root for child in root.get_children(): var found := find_label(child, text_fragment) if found != null: return found return null # 控件实测几何(shot_probe checker 文字框取样坐标源——禁硬编码像素,分辨率档位自适应) static func label_rect(root: Node, text_fragment: String) -> Dictionary: if root == null: return {} if root is Label and String(root.text).contains(text_fragment): return {"x": root.global_position.x, "y": root.global_position.y, "w": root.size.x, "h": root.size.y} for child in root.get_children(): var r := label_rect(child, text_fragment) if not r.is_empty(): return r return {} # 按脚本名寻址(resource_path contains——shot_probe hero 面板锚) static func find_by_script(root: Node, script_name: String) -> Node: if root == null: return null var s: Script = root.get_script() if s != null and String(s.resource_path).contains(script_name): return root for child in root.get_children(): var r := find_by_script(child, script_name) if r != null: return r return null static func _text_match(text: String, needle: String, mode: String) -> bool: return text.begins_with(needle) if mode == "prefix" else text.contains(needle) # ── 存档/设置/元进度三面隔离(探针禁碰真实档 user://save.json——SaveManager 路径注入纪律) ── # tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。 # mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注 #(隔离 cfg 名 = _settings.cfg,与被收口探针原文件名逐一同名); # 「继续上局」按钮重刷;swap_save = 是否换注全局 SaveManager(营地先行段只换 meta 侧不换 SaveManager)。 # 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。 # SaveManager/MetaProgression 是 autoload 单例——隔离面走单例原地重置 #(attach_path 沿 RngService.set_master_seed 先例;MetaProgression 持 save 引用透传, # swap_save=false 分支换 meta 侧引用=独立 iso 实例,只换 meta 不换 save_manager)。 static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false, swap_save: bool = true): # refresh_resume 参数已随卡② _refresh_resume_button 删除退役(mn7) var save_path := "user://%s.json" % tag for p in [save_path, save_path + ".tmp", "user://%s_settings.cfg" % tag]: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) var iso = SaveManagerScript.new(save_path) iso.load_from_disk() if mark_first_done: iso.meta_state.mark_first_adventure_done() # --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析 var sm = main.get_node("/root/SaveManager") if swap_save: sm.slots.attach_path(save_path) if mark_first_done: sm.meta_state.mark_first_adventure_done() iso = sm else: main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动 if with_settings: main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) return iso # ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)---- # 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对 #(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 先例式)。 # 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。 # coverage_hook = 真输入覆盖登记件(probe_coverage_recorder.gd)挂点:本件三个 push 口是探针 # 真输入的单点通道,push 前通知登记件置旗标——命中记账只认旗标在位期间(信号直发不入账)。 static var coverage_hook: Object = null static func push_click(main, ctrl: Control) -> void: push_click_at(main, ctrl.get_global_rect().get_center()) static func push_click_at(main, pos: Vector2) -> void: if coverage_hook != null: coverage_hook.mark_real_input() var down := InputEventMouseButton.new() down.position = pos down.global_position = pos down.button_index = MOUSE_BUTTON_LEFT down.pressed = true main.get_viewport().push_input(down, true) var up := InputEventMouseButton.new() up.position = pos up.global_position = pos up.button_index = MOUSE_BUTTON_LEFT up.pressed = false main.get_viewport().push_input(up, true) # 悬停真输入(tooltip/悬停态断言的承载——motion 进管线触发 mouse_entered/exited 沿) static func push_motion(main, pos: Vector2) -> void: if coverage_hook != null: coverage_hook.mark_real_input() var mv := InputEventMouseMotion.new() mv.position = pos mv.global_position = pos main.get_viewport().push_input(mv, true) # 滚轮真输入(dir=+1 向下翻 / -1 向上翻——折叠区行滚入视野的唯一真输入通道)。 # wheel 合成必须 press+release 成对:硬件滚轮虽无 release 沿,但引擎 GUI 会把按键事件的 # press 记为鼠标焦点且等 release 才释放——只发 press 会把焦点钉死在按下点控件上, # 后续左键点击全被路由给焦点控件(实证:滚轮后 fork/返回营地两钮点击全部失效)。 static func push_wheel(main, pos: Vector2, dir: int) -> void: if coverage_hook != null: coverage_hook.mark_real_input() var idx := MOUSE_BUTTON_WHEEL_DOWN if dir > 0 else MOUSE_BUTTON_WHEEL_UP var w := InputEventMouseButton.new() w.position = pos w.global_position = pos w.button_index = idx w.pressed = true main.get_viewport().push_input(w, true) var w2 := InputEventMouseButton.new() w2.position = pos w2.global_position = pos w2.button_index = idx w2.pressed = false main.get_viewport().push_input(w2, true) # 窗板定位(窗壳视图子树固定链:遮罩 ColorRect → CenterContainer → 窗板 PanelContainer—— # screen_flow_probe._window_panel 同式,探针族共用单点) static func window_panel(view: Control) -> PanelContainer: if view == null or view.get_child_count() == 0: return null var dim := view.get_child(0) if dim.get_child_count() == 0: return null var center := dim.get_child(0) if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer): return null return center.get_child(0) as PanelContainer # 折叠区行滚入窗板可视区(真输入滚轮驱动;滚动不重建行,rect 随滚更新)。 # 判据:钮 rect 整体落在窗板内(上缘留 8px、下缘让开底栏 64px——返回营地钮带高约 56px)。 # 保险丝 60 次滚叶防异常内容死循环;窗板缺失或钮句柄失效即返回(调用方前置断言兜底)。 static func scroll_button_into_view(main, view: Control, btn: Control) -> void: var panel := window_panel(view) if panel == null or btn == null: return var guard := 60 while guard > 0: var r := btn.get_global_rect() var pr := panel.get_global_rect() if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 64.0: return var down := r.end.y > pr.end.y - 64.0 push_wheel(main, pr.get_center(), 1 if down else -1) guard -= 1 await main.get_tree().process_frame # 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批 # R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件 # 自身承载,被测面按 relative 逐帧平移) static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) -> void: var down := InputEventMouseButton.new() down.position = pos down.global_position = pos down.button_index = MOUSE_BUTTON_LEFT down.pressed = true main.get_viewport().push_input(down, true) for i in range(1, steps + 1): var cur := pos + delta * float(i) / float(steps) var mv := InputEventMouseMotion.new() mv.position = cur mv.global_position = cur mv.relative = delta / float(steps) main.get_viewport().push_input(mv, true) var up := InputEventMouseButton.new() up.position = pos + delta up.global_position = pos + delta up.button_index = MOUSE_BUTTON_LEFT up.pressed = false main.get_viewport().push_input(up, true) # 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面, # 本工具只发原始滚轮档)。press+release 成对(同 push_wheel 注——只发 press 钉死鼠标焦点)。 static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void: var ev := InputEventMouseButton.new() ev.position = pos ev.global_position = pos ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP ev.pressed = true main.get_viewport().push_input(ev, true) var ev2 := InputEventMouseButton.new() ev2.position = pos ev2.global_position = pos ev2.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP ev2.pressed = false main.get_viewport().push_input(ev2, true) # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽 # user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点); # 驱动后写盘目标=隔离槽文件(持久化断言读 user://_slot_1.json)。返回 false=新档钮缺失(驱动中断)。 static func open_camp_via_savesel(main, tag: String) -> bool: var sm = main.get_node("/root/SaveManager") sm.slots.attach_slot_template("user://%s_slot_%%d.json" % tag) for i in range(1, 4): # 夹具清场——空槽起(陈旧槽文件会翻有档态、新档钮缺席) for suf in ["", ".tmp", ".corrupt.bak"]: var sp: String = sm.slots.slot_path(i) + suf if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) main.open_save_select() await main.get_tree().process_frame # 排版帧——真点击命中的是控件 rect,开层同帧容器未排(emit 无此依赖) var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if new_btn == null: return false push_click(main, new_btn) # 真输入 → _on_save_select_new → create_slot+open_camp return true