extends SceneTree const EntryShell = preload("res://entry/entry_shell.gd") const SettingsStore = preload("res://entry/settings_store.gd") const DraftService = preload("res://entry/initial_company_draft_service.gd") const ContractService = preload("res://entry/guild_contract_service.gd") const FormationService = preload("res://entry/formation_risk_service.gd") const BattleService = preload("res://entry/battle_service.gd") const FIXTURES := [ {"size": Vector2i(760, 360), "locale": "zh_CN"}, {"size": Vector2i(844, 390), "locale": "en"}, {"size": Vector2i(1280, 720), "locale": "zh_CN"}, {"size": Vector2i(1280, 720), "locale": "en"}, ] const STATES := ["battle_opening", "battle_live", "result", "knockout", "reward", "recovery", "next_market", "restart_market"] var failures: Array[String] = [] var capture_count := 0 var output_root := "" func _initialize() -> void: call_deferred("run_capture") func run_capture() -> void: output_root = OS.get_environment("ABG_EVIDENCE_DIR") if output_root.is_empty(): output_root = "/tmp/abg-m12-capture" output_root = output_root.path_join("captures") DirAccess.make_dir_recursive_absolute(output_root) for index in range(FIXTURES.size()): await _capture_fixture(index, FIXTURES[index]) if failures.is_empty() and capture_count == FIXTURES.size() * STATES.size(): print("ABG_M12_CAPTURE_OK fixtures=4 captures=%d states=battle_opening,battle_live,result,knockout,reward,recovery,next_market,restart_market scale=100 restart=market strict=0" % capture_count) quit(0) else: for failure in failures: push_error(failure) quit(1) func _capture_fixture(index: int, fixture: Dictionary) -> void: var draft_path := "user://m12_capture_%d_draft.json" % index var company_path := "user://m12_capture_%d_company.json" % index _cleanup_path(draft_path, company_path) var prepared := _create_prepared_company(draft_path, company_path, "97531eca86420bdf97531eca86420bd%d" % index) if prepared.is_empty(): failures.append("fixture %d did not prepare a Company" % index) return var option_id := String(prepared.active_contract.formation.risk_options[1].risk_option_id) var service := BattleService.new() service.company_path = company_path var started: Dictionary = service.begin_battle(("2468ace013579bdf2468ace013579b%d" % index).sha256_text().substr(0, 32), option_id) if not started.ok: failures.append("fixture %d did not start Battle" % index) _cleanup_path(draft_path, company_path) return var viewport := SubViewport.new() viewport.name = "M12CaptureViewport%d" % index viewport.size = fixture.size viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS root.add_child(viewport) var shell := EntryShell.new() shell.auto_start = false shell.initial_company_service.path = draft_path shell.initial_company_service.company_path = company_path viewport.add_child(shell) await _frames(3) var settings := SettingsStore.new().defaults() settings.setup_completed = true settings.locale = String(fixture.locale) settings.text_scale = 1.0 shell.settings = settings shell.setup_draft = settings.duplicate(true) shell.show_battle_opening(started.company, false) await _frames(5) await _capture_state(viewport, shell, index, String(fixture.locale), "battle_opening") var start := shell.find_child("BattleStartObservation", true, false) as Button if start == null: failures.append("fixture %d missing BattleStartObservation" % index) else: # The route marker already verifies parsed touch on the opening page. For # capture, hand the same persisted receipt to BAT-002 directly so the # SubViewport fixture does not depend on a window-level pointer bridge. var battle: Dictionary = started.company.get("active_contract", {}).get("battle_state", {}) var observed: Dictionary = service.begin_observation(String(battle.get("operation_id", "")), String(battle.get("snapshot_hash", ""))) if not observed.ok: failures.append("fixture %d could not begin observation" % index) else: shell.show_battle(observed.company, false) shell.battle_paused = false shell._schedule_battle_playback(shell.battle_playback_generation) await _frames(4) await _capture_state(viewport, shell, index, String(fixture.locale), "battle_live") var guard := 0 while shell.battle_live_state != "outcome_locked" and guard < 60: if shell.battle_paused: shell.battle_paused = false shell.advance_battle_phase() await _frames(1) guard += 1 if shell.battle_live_state != "outcome_locked": failures.append("fixture %d did not reach outcome lock" % index) else: var next := shell.find_child("BattleLiveNextBoundary", true, false) as Button if next == null: failures.append("fixture %d missing OUT-001 boundary" % index) else: shell._battle_live_commit_outcome() await _frames(4) await _capture_state(viewport, shell, index, String(fixture.locale), "result") var continue_button := shell.find_child("BattleResultContinue", true, false) as Button if continue_button == null: failures.append("fixture %d missing Result Continue" % index) else: shell.continue_from_battle_result() await _frames(4) if shell.current_screen == "battle_knockout_review": await _capture_state(viewport, shell, index, String(fixture.locale), "knockout") var acknowledge := shell.find_child("BattleKnockoutAcknowledge", true, false) as Button if acknowledge == null: failures.append("fixture %d missing Knockout acknowledgement" % index) else: shell.acknowledge_battle_knockout(shell.guild_company) await _frames(4) if shell.current_screen != "battle_reward_choice": failures.append("fixture %d did not reach Reward Choice" % index) else: await _capture_state(viewport, shell, index, String(fixture.locale), "reward") var reward_button := _first_reward_button(shell) if reward_button == null: failures.append("fixture %d has no reward option" % index) else: var reward_id := String(reward_button.name).trim_prefix("BattleRewardOption_") shell.select_battle_reward(reward_id) await _frames(2) var claim := shell.find_child("BattleRewardClaim", true, false) as Button if claim == null: failures.append("fixture %d missing Reward Claim" % index) else: shell.commit_battle_reward() await _frames(4) if shell.current_screen != "battle_recovery": failures.append("fixture %d did not reach Recovery" % index) else: await _capture_state(viewport, shell, index, String(fixture.locale), "recovery") var commit := shell.find_child("BattleRecoveryCommit", true, false) as Button if commit == null: failures.append("fixture %d missing Recovery Commit" % index) else: shell.commit_battle_recovery() await _frames(4) if shell.current_screen != "market": failures.append("fixture %d did not reach next Market" % index) else: await _capture_state(viewport, shell, index, String(fixture.locale), "next_market") var loaded: Dictionary = service.load_company() if not loaded.ok: failures.append("fixture %d cannot reload after recovery" % index) else: shell.queue_free() await _frames(4) var resumed := EntryShell.new() resumed.auto_start = false resumed.initial_company_service.path = draft_path resumed.initial_company_service.company_path = company_path viewport.add_child(resumed) await _frames(3) resumed.settings = settings resumed.setup_draft = settings.duplicate(true) resumed._open_company_destination(loaded.company) await _frames(4) if resumed.current_screen != "market": failures.append("fixture %d restart did not open Market" % index) else: await _capture_state(viewport, resumed, index, String(fixture.locale), "restart_market") resumed.queue_free() viewport.queue_free() await _frames(5) _cleanup_path(draft_path, company_path) func _capture_state(viewport: SubViewport, shell: Control, fixture_index: int, locale: String, state: String) -> void: if shell.current_screen == "battle_result": _expect_node(shell, "BattleResultObjective", state) elif shell.current_screen == "battle_knockout_review": _expect_node(shell, "BattleKnockoutCurrentGap", state) elif shell.current_screen == "battle_reward_choice": _expect_node(shell, "BattleRewardOptions", state) elif shell.current_screen == "battle_recovery": _expect_node(shell, "BattleRecoveryRule", state) await RenderingServer.frame_post_draw var image := viewport.get_texture().get_image() if image.is_empty() or image.get_size() != viewport.size: failures.append("%s fixture %d has invalid frame dimensions" % [state, fixture_index]) return var path := output_root.path_join("fixture%d-%s-%s-100.png" % [fixture_index, locale, state]) if image.save_png(path) != OK: failures.append("could not save %s" % path) else: capture_count += 1 func _expect_node(shell: Control, node_name: String, state: String) -> void: if shell.find_child(node_name, true, false) == null: failures.append("%s is missing %s" % [state, node_name]) func _first_reward_button(shell: Control) -> Button: var options := shell.find_child("BattleRewardOptions", true, false) if options == null: return null for child in options.get_children(): if child is Button and String(child.name).begins_with("BattleRewardOption_"): return child as Button return null func _create_prepared_company(draft_path: String, company_path: String, seed: String) -> Dictionary: var draft_service := DraftService.new() draft_service.path = draft_path draft_service.company_path = company_path var draft: Dictionary = draft_service.begin_or_load(seed) if not draft.ok: return {} draft_service.set_selection([String(draft.draft.offers[0].offer_id), String(draft.draft.offers[1].offer_id)]) if not draft_service.commit_company().ok: return {} var contract_service := ContractService.new() contract_service.company_path = company_path var created: Dictionary = contract_service.create_contract(("ctr|%s" % seed).sha256_text().substr(0, 32), "CTR-R1-01", "standard") if not created.ok: return {} var offers: Array = created.company.active_contract.market_board.recruit_offers contract_service.hire_recruit(("hire1|%s" % seed).sha256_text().substr(0, 32), String(offers[0].source_offer_id), 14) var hired: Dictionary = contract_service.hire_recruit(("hire2|%s" % seed).sha256_text().substr(0, 32), String(offers[1].source_offer_id), 14) if not hired.ok: return {} var formation_service := FormationService.new() formation_service.company_path = company_path var begun: Dictionary = formation_service.begin_formation() var saved: Dictionary = formation_service.save_order(("order|%s" % seed).sha256_text().substr(0, 32), begun.formation.base_front_to_rear_ids) var ready: Dictionary = formation_service.accept_readiness(("ready|%s" % seed).sha256_text().substr(0, 32), String(saved.receipt.order_hash)) return ready.company if ready.ok else {} func _touch(control: Control) -> void: if control == null: return var center := control.get_global_rect().get_center() var press := InputEventScreenTouch.new() press.position = center press.pressed = true press.index = 0 Input.parse_input_event(press) await _frames(2) var release := InputEventScreenTouch.new() release.position = center release.pressed = false release.index = 0 Input.parse_input_event(release) await _frames(5) func _frames(count: int) -> void: for _index in range(count): await process_frame func _cleanup_path(draft_path: String, company_path: String) -> void: for base in [draft_path, company_path]: for suffix in ["", ".bak", ".candidate", ".bak.candidate", ".corrupt"]: var path := "%s%s" % [base, suffix] if FileAccess.file_exists(path): DirAccess.remove_absolute(path)