360 lines
15 KiB
GDScript
360 lines
15 KiB
GDScript
extends SceneTree
|
|
|
|
## M63 entry-page visual probe (W5/DEV-1R). It captures the migrated entry
|
|
## states at the shipped fixtures with the runtime content, the chrome-only
|
|
## static view and the content-only dynamic view on a neutral field, and it
|
|
## runs the shared ink checker on every build. Boot is strict from M63-A; the
|
|
## later entry pages join this probe as they migrate.
|
|
|
|
const EntryShell = preload("res://entry/entry_shell.gd")
|
|
const SettingsStore = preload("res://entry/settings_store.gd")
|
|
const InkCheck = preload("res://tools/ui_ink_check.gd")
|
|
|
|
const SIZES := [Vector2i(1280, 720)]
|
|
# Owner viewport policy: 1280x720 is the single design authority and owns the
|
|
# strict ink verdict; smaller landscape sizes are smoke captures only.
|
|
const AUTHORITY_SIZE := Vector2i(1280, 720)
|
|
const LOCALES := ["zh_CN", "en"]
|
|
# Accessibility pass: 1.0 plus the shipped 130% text scale (M73).
|
|
const TEXT_SCALES := [1.0, 1.3]
|
|
const STATES := ["running", "error"]
|
|
const TITLE_DRAFT_PATH := "user://m63_title_probe_draft.json"
|
|
const TITLE_COMPANY_PATH := "user://m63_title_probe_company.json"
|
|
const TITLE_COMPANY_BACKUP_PATH := "user://m63_title_probe_company.json.bak"
|
|
const TITLE_SEED := "13579bdf2468ace00eca8642fdb97531"
|
|
const DraftService = preload("res://entry/initial_company_draft_service.gd")
|
|
|
|
var output_root := ""
|
|
var ink_scanned := 0
|
|
var probe_text_scale := 1.0
|
|
var title_recoverable_ok := true
|
|
var ink_violations: Array = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
output_root = OS.get_environment("ABG_EVIDENCE_DIR")
|
|
if output_root.is_empty():
|
|
push_error("M63 entry probe requires the owned test-attempt runner")
|
|
quit(1)
|
|
return
|
|
DirAccess.make_dir_recursive_absolute(output_root)
|
|
var self_test := InkCheck.self_test(root)
|
|
if self_test.size() < 3:
|
|
push_error("M63 ink self-test expected at least 3 violations, got %d" % self_test.size())
|
|
quit(1)
|
|
return
|
|
print("ABG_M63_INK_SELF_TEST_OK violations=%d" % self_test.size())
|
|
for size in SIZES:
|
|
for locale in LOCALES:
|
|
for text_scale in TEXT_SCALES:
|
|
probe_text_scale = float(text_scale)
|
|
for state in STATES:
|
|
await _capture_boot(size, locale, state)
|
|
await _capture_setup(size, locale)
|
|
await _capture_title(size, locale, "fresh")
|
|
await _capture_title(size, locale, "recoverable")
|
|
if size == AUTHORITY_SIZE:
|
|
await _capture_settings(size, locale, "display")
|
|
await _capture_settings(size, locale, "audio")
|
|
await _capture_opening(size, locale, "choice")
|
|
await _capture_opening(size, locale, "inspect")
|
|
await _capture_opening(size, locale, "compare")
|
|
await _capture_opening(size, locale, "review")
|
|
var report_path := output_root.path_join("ink-violations-m63-entry.json")
|
|
var file := FileAccess.open(report_path, FileAccess.WRITE)
|
|
if file != null:
|
|
file.store_string(JSON.stringify(ink_violations, " "))
|
|
file.close()
|
|
if ink_violations.is_empty():
|
|
print("ABG_M63_ENTRY_INK_OK scanned=%d violations=0" % ink_scanned)
|
|
else:
|
|
for violation in ink_violations:
|
|
push_error("M63 ink violation %s %s: %s" % [violation.node, violation.kind, violation.detail])
|
|
print("ABG_M63_ENTRY_INK_FAIL scanned=%d violations=%d" % [ink_scanned, ink_violations.size()])
|
|
quit(1)
|
|
return
|
|
if title_recoverable_ok:
|
|
print("ABG_M63_TITLE_RECOVERABLE_OK new_game=enabled continue=enabled")
|
|
print("ABG_M63_ENTRY_VISUAL_PROBE_OK sizes=1 locales=2 pages=boot,setup,title,settings,opening states=running,error,fresh,recoverable,display,audio,choice,inspect,compare,review views=static,dynamic,final")
|
|
quit(0)
|
|
|
|
|
|
func _capture_boot(size: Vector2i, locale: String, state: String) -> void:
|
|
var viewport := SubViewport.new()
|
|
viewport.size = size
|
|
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(viewport)
|
|
var shell := EntryShell.new()
|
|
shell.auto_start = false
|
|
viewport.add_child(shell)
|
|
await _frames(3)
|
|
shell.settings = SettingsStore.new().defaults()
|
|
shell.settings.locale = locale
|
|
shell.settings.text_scale = probe_text_scale
|
|
shell.boot_snapshot = (
|
|
{"state": "error", "completed": 2, "total": 4, "step": "preferences", "code": "SETTINGS_FILE_INVALID"}
|
|
if state == "error"
|
|
else {"state": "running", "completed": 2, "total": 4, "step": "preferences", "code": ""}
|
|
)
|
|
shell._render_boot_state()
|
|
await _frames(4)
|
|
_scan_ink(shell, locale, "boot-%dx%d-%s" % [size.x, size.y, state], size == AUTHORITY_SIZE)
|
|
await _save(viewport, "boot-%dx%d-%s-%s" % [size.x, size.y, locale, state], "final")
|
|
# static: chrome only, runtime copy hidden.
|
|
for node_name in ["BootIdentity", "BootStatus", "BootDetail", "BootProgressValue", "BootDiagnostics"]:
|
|
var control := shell.find_child(node_name, true, false) as CanvasItem
|
|
if control != null:
|
|
control.visible = false
|
|
await _frames(2)
|
|
await _save(viewport, "boot-%dx%d-%s-%s" % [size.x, size.y, locale, state], "static")
|
|
for node_name in ["BootIdentity", "BootStatus", "BootDetail", "BootProgressValue", "BootDiagnostics"]:
|
|
var control := shell.find_child(node_name, true, false) as CanvasItem
|
|
if control != null:
|
|
control.visible = true
|
|
# dynamic: content only on a neutral field (world hidden).
|
|
var world := shell.find_child("BootWorld", true, false) as CanvasItem
|
|
var shade := shell.find_child("BootWorldShade", true, false) as CanvasItem
|
|
if world != null:
|
|
world.visible = false
|
|
if shade != null:
|
|
shade.visible = false
|
|
var neutral := ColorRect.new()
|
|
neutral.name = "M63NeutralField"
|
|
neutral.color = Color("#5b6165")
|
|
neutral.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
neutral.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
shell.add_child(neutral)
|
|
shell.move_child(neutral, 0)
|
|
await _frames(2)
|
|
await _save(viewport, "boot-%dx%d-%s-%s" % [size.x, size.y, locale, state], "dynamic")
|
|
viewport.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _capture_setup(size: Vector2i, locale: String) -> void:
|
|
var viewport := SubViewport.new()
|
|
viewport.size = size
|
|
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(viewport)
|
|
var shell := EntryShell.new()
|
|
shell.auto_start = false
|
|
viewport.add_child(shell)
|
|
await _frames(3)
|
|
shell.settings = SettingsStore.new().defaults()
|
|
shell.settings.locale = locale
|
|
shell.settings.text_scale = probe_text_scale
|
|
shell.size = Vector2(size)
|
|
shell.show_first_launch()
|
|
await _frames(5)
|
|
# The shell lays out on resize; drive it once for the offscreen capture.
|
|
shell._layout_first_launch()
|
|
shell._refresh_first_launch_labels()
|
|
await _frames(3)
|
|
_scan_ink(shell, locale, "setup-%dx%d" % [size.x, size.y], size == AUTHORITY_SIZE)
|
|
await _save(viewport, "setup-%dx%d-%s" % [size.x, size.y, locale], "final")
|
|
for node_name in ["FirstLaunchHeading", "FirstLaunchSubheading", "FirstLaunchPreviewHeading", "SetupPreviewTitle", "SetupPreviewDetail"]:
|
|
var control := shell.find_child(node_name, true, false) as CanvasItem
|
|
if control != null:
|
|
control.visible = false
|
|
for row_name in ["SetupLanguage", "SetupSafeArea", "SetupTextScale", "SetupReducedMotion", "SetupReducedFlashes", "SetupCaptions"]:
|
|
var row := shell.find_child(row_name, true, false) as Control
|
|
if row != null:
|
|
var label := row.get_node_or_null("RowLabel") as CanvasItem
|
|
var value := row.get_node_or_null("ValuePlate") as CanvasItem
|
|
if label != null:
|
|
label.visible = false
|
|
if value != null:
|
|
value.visible = false
|
|
await _frames(2)
|
|
await _save(viewport, "setup-%dx%d-%s" % [size.x, size.y, locale], "static")
|
|
viewport.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _capture_title(size: Vector2i, locale: String, state: String) -> void:
|
|
var viewport := SubViewport.new()
|
|
viewport.size = size
|
|
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(viewport)
|
|
var shell := EntryShell.new()
|
|
shell.auto_start = false
|
|
viewport.add_child(shell)
|
|
await _frames(3)
|
|
shell.settings = SettingsStore.new().defaults()
|
|
shell.settings.setup_completed = true
|
|
shell.settings.locale = locale
|
|
shell.settings.text_scale = probe_text_scale
|
|
shell.initial_company_service.path = TITLE_DRAFT_PATH
|
|
shell.initial_company_service.company_path = TITLE_COMPANY_PATH
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_PATH))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_BACKUP_PATH))
|
|
if state == "recoverable":
|
|
# M60-I04: only a leftover backup exists; New Game must stay usable.
|
|
# Build a real, valid record through the service, then move the primary
|
|
# aside so recovery takes the backup path.
|
|
var service = DraftService.new()
|
|
service.path = TITLE_DRAFT_PATH
|
|
service.company_path = TITLE_COMPANY_PATH
|
|
var created: Dictionary = service.begin_or_load(TITLE_SEED)
|
|
if created.ok:
|
|
var offers: Array = created.draft.offers
|
|
service.set_selection([String(offers[0].offer_id), String(offers[2].offer_id)])
|
|
service.commit_company()
|
|
# Move the committed record aside so recovery genuinely takes the
|
|
# backup path (a lingering primary would load normally).
|
|
var primary := FileAccess.open(TITLE_COMPANY_PATH, FileAccess.READ)
|
|
if primary != null:
|
|
var company_text := primary.get_as_text()
|
|
primary.close()
|
|
var backup := FileAccess.open(TITLE_COMPANY_BACKUP_PATH, FileAccess.WRITE)
|
|
backup.store_string(company_text)
|
|
backup.close()
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_PATH))
|
|
shell.size = Vector2(size)
|
|
shell.show_title()
|
|
await _frames(4)
|
|
var new_game := shell.find_child("TitleNewGame", true, false) as Button
|
|
var continue_button := shell.find_child("TitleContinue", true, false) as Button
|
|
if state == "recoverable" and size == AUTHORITY_SIZE:
|
|
if new_game == null or new_game.disabled:
|
|
print("ERROR: M63 M60-I04 regression: New Game disabled with a recoverable record")
|
|
title_recoverable_ok = false
|
|
if continue_button == null or continue_button.disabled:
|
|
print("ERROR: M63 M60-I04 regression: Continue disabled with a recoverable record")
|
|
title_recoverable_ok = false
|
|
if state == "fresh" and size == AUTHORITY_SIZE:
|
|
if new_game == null or new_game.disabled:
|
|
print("ERROR: M63 Title: New Game disabled with no local record")
|
|
title_recoverable_ok = false
|
|
_scan_ink(shell, locale, "title-%s-%dx%d" % [state, size.x, size.y], size == AUTHORITY_SIZE)
|
|
await _save(viewport, "title-%s-%dx%d-%s" % [state, size.x, size.y, locale], "final")
|
|
for node_name in ["TitleOverlay", "TitleOverline", "TitleIdentity", "TitleChronicle", "TitleNotice", "TitleSaveSummary", "TitleContinueReason"]:
|
|
var control := shell.find_child(node_name, true, false) as CanvasItem
|
|
if control != null:
|
|
control.visible = false
|
|
await _frames(2)
|
|
await _save(viewport, "title-%s-%dx%d-%s" % [state, size.x, size.y, locale], "static")
|
|
if state == "recoverable":
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_BACKUP_PATH))
|
|
viewport.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _capture_opening(size: Vector2i, locale: String, mode: String) -> void:
|
|
var viewport := SubViewport.new()
|
|
viewport.size = size
|
|
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(viewport)
|
|
var shell := EntryShell.new()
|
|
shell.auto_start = false
|
|
viewport.add_child(shell)
|
|
await _frames(3)
|
|
shell.settings = SettingsStore.new().defaults()
|
|
shell.settings.setup_completed = true
|
|
shell.settings.locale = locale
|
|
shell.initial_company_service.path = TITLE_DRAFT_PATH
|
|
shell.initial_company_service.company_path = TITLE_COMPANY_PATH
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_DRAFT_PATH))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_DRAFT_PATH + ".bak"))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_PATH))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_BACKUP_PATH))
|
|
shell.size = Vector2(size)
|
|
shell.show_initial_company()
|
|
var page = shell.initial_company_page
|
|
await _frames(4)
|
|
if page != null and mode == "inspect":
|
|
page.inspect_candidate(1)
|
|
await _frames(4)
|
|
if page != null and mode == "compare":
|
|
page.compare_a_index = 0
|
|
page.compare_b_index = 2
|
|
page.show_compare()
|
|
await _frames(4)
|
|
if page != null and mode == "review":
|
|
var offers: Array = page.draft.offers
|
|
page.toggle_candidate(0)
|
|
page.toggle_candidate(2)
|
|
page.request_review()
|
|
await _frames(4)
|
|
_scan_ink(shell, locale, "opening-%s-%dx%d" % [mode, size.x, size.y], true)
|
|
await _save(viewport, "opening-%s-%dx%d-%s" % [mode, size.x, size.y, locale], "final")
|
|
for node_name in ["ChoiceHeader", "CandidateInfo", "ChoiceNotice", "ReviewNotice", "ReviewUnchosen"]:
|
|
var control := shell.find_child(node_name, true, false) as CanvasItem
|
|
if control != null:
|
|
control.visible = false
|
|
await _frames(2)
|
|
await _save(viewport, "opening-%s-%dx%d-%s" % [mode, size.x, size.y, locale], "static")
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_DRAFT_PATH))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_DRAFT_PATH + ".bak"))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_PATH))
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(TITLE_COMPANY_BACKUP_PATH))
|
|
viewport.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _capture_settings(size: Vector2i, locale: String, category: String) -> void:
|
|
var viewport := SubViewport.new()
|
|
viewport.size = size
|
|
viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(viewport)
|
|
var shell := EntryShell.new()
|
|
shell.auto_start = false
|
|
viewport.add_child(shell)
|
|
await _frames(3)
|
|
shell.settings = SettingsStore.new().defaults()
|
|
shell.settings.setup_completed = true
|
|
shell.settings.locale = locale
|
|
shell.size = Vector2(size)
|
|
shell.show_settings(category)
|
|
await _frames(4)
|
|
_scan_ink(shell, locale, "settings-%s-%dx%d" % [category, size.x, size.y], true)
|
|
await _save(viewport, "settings-%s-%dx%d-%s" % [category, size.x, size.y, locale], "final")
|
|
for node_name in ["SettingsTitle", "SettingsCaller", "SettingsNotice"]:
|
|
var control := shell.find_child(node_name, true, false) as CanvasItem
|
|
if control != null:
|
|
control.visible = false
|
|
var content := shell.find_child("SettingsContent", true, false) as Control
|
|
if content != null:
|
|
for child in content.get_children():
|
|
var canvas := child as CanvasItem
|
|
if canvas != null:
|
|
canvas.visible = false
|
|
await _frames(2)
|
|
await _save(viewport, "settings-%s-%dx%d-%s" % [category, size.x, size.y, locale], "static")
|
|
viewport.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _scan_ink(view: Control, locale: String, label: String, strict := true) -> void:
|
|
var violations := InkCheck.scan(view)
|
|
if not strict:
|
|
# Smoke sizes report for the record but do not gate: the authority size
|
|
# owns the strict verdict (owner viewport policy, 2026-09-13).
|
|
for violation in violations:
|
|
print("ABG_M63_SMOKE_NOTE %s %s %s" % [label, locale, String(violation.get("kind", "?"))])
|
|
return
|
|
ink_scanned += 1
|
|
for violation in violations:
|
|
violation["locale"] = locale
|
|
violation["fixture"] = label
|
|
ink_violations.append(violation)
|
|
|
|
|
|
func _save(viewport: SubViewport, stem: String, mode: String) -> void:
|
|
await RenderingServer.frame_post_draw
|
|
var texture := viewport.get_texture()
|
|
if texture == null:
|
|
return
|
|
var image := texture.get_image()
|
|
if image != null and not image.is_empty():
|
|
image.save_png(output_root.path_join("%s-%s.png" % [stem, mode]))
|
|
|
|
|
|
func _frames(count: int) -> void:
|
|
for _index in count:
|
|
await process_frame
|