203 lines
6.7 KiB
GDScript
203 lines
6.7 KiB
GDScript
@tool
|
|
extends EditorPlugin
|
|
|
|
## Temporary v3-compatible entry point delivered through the signed legacy
|
|
## updater asset. It owns presentation only. Verification and staging run in
|
|
## the detached bridge node; the value-only coordinator survives this plugin
|
|
## handing off to the shared in-editor activation runner.
|
|
|
|
const MigrationBridge := preload("res://addons/godot_ai/migration_bridge.gd")
|
|
const MigrationCoordinator := preload("res://addons/godot_ai/migration_coordinator.gd")
|
|
const MigrationFallback := preload("res://addons/godot_ai/migration_fallback.gd")
|
|
const FLOOR_REFUSAL := "Godot AI v4 migration requires Godot 4.7 or newer; bridge remains inactive."
|
|
const FLOOR_MESSAGE := "Godot AI v4 requires Godot 4.7 or newer. Upgrade Godot, reopen this project, then click Retry."
|
|
const FLOOR_RESTORING := (
|
|
"Godot AI v4 requires Godot 4.7 or newer. Restoring the previous Godot AI version; "
|
|
+ "upgrade Godot to update."
|
|
)
|
|
const EDITOR_REQUIRED := (
|
|
"Godot AI v4 migration needs the interactive Godot editor. "
|
|
+ "Reopen the project in the editor (not headless or exported), then click Retry migration."
|
|
)
|
|
|
|
var _dock: VBoxContainer
|
|
var _label: Label
|
|
var _retry: Button
|
|
var _bridge
|
|
var _coordinator
|
|
|
|
|
|
func _enter_tree() -> void:
|
|
_build_dock()
|
|
var prior := _bridge_status()
|
|
if not prior.is_empty():
|
|
_present_error(str(prior.get("error", "The previous migration attempt did not finish.")))
|
|
return
|
|
if not _editor_available():
|
|
## Headless imports and exports must never mutate the tree, not even
|
|
## to restore final v3; the next interactive start handles it.
|
|
_present_state(EDITOR_REQUIRED, true, false)
|
|
return
|
|
if not _meets_floor():
|
|
print(FLOOR_REFUSAL)
|
|
## The user's updater already overlaid the capsule over their v3 tree.
|
|
## Put the final v3 back so nothing is lost; without the fallback (an
|
|
## older capsule) the bridge can only wait for a newer Godot.
|
|
if MigrationFallback.available():
|
|
_present_state(FLOOR_RESTORING, false, false)
|
|
var fallback: Node = MigrationFallback.new()
|
|
_parent_node().add_child(fallback)
|
|
fallback.start()
|
|
return
|
|
_present_state(FLOOR_MESSAGE, true, false)
|
|
return
|
|
_start_migration.call_deferred()
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
_free_bridge()
|
|
if _coordinator != null and is_instance_valid(_coordinator):
|
|
## The coordinator outlives this plugin on purpose; it disables the
|
|
## capsule and keeps running. Only stop listening to it.
|
|
if _coordinator.state_changed.is_connected(_present_state):
|
|
_coordinator.state_changed.disconnect(_present_state)
|
|
_coordinator = null
|
|
if _dock != null and is_instance_valid(_dock):
|
|
remove_control_from_docks(_dock)
|
|
_dock.queue_free()
|
|
_dock = null
|
|
|
|
|
|
func _build_dock() -> void:
|
|
_dock = VBoxContainer.new()
|
|
_dock.name = "Godot AI"
|
|
_label = Label.new()
|
|
_label.text = "Preparing the signed Godot AI v4 migration…"
|
|
_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
_label.add_theme_font_size_override("font_size", 15)
|
|
_dock.add_child(_label)
|
|
_retry = Button.new()
|
|
_retry.text = "Retry migration"
|
|
_retry.visible = false
|
|
_retry.pressed.connect(_on_retry)
|
|
_dock.add_child(_retry)
|
|
add_control_to_dock(DOCK_SLOT_RIGHT_UL, _dock)
|
|
|
|
|
|
func _start_migration() -> void:
|
|
if _bridge != null or _coordinator != null:
|
|
return
|
|
_clear_bridge_status()
|
|
_retry.visible = false
|
|
_label.text = "Preparing the signed Godot AI v4 migration…"
|
|
print("MCP | v3 bridge verifying and staging the signed v4 tree")
|
|
_bridge = MigrationBridge.new()
|
|
_bridge.state_changed.connect(_present_state)
|
|
_bridge.prepared.connect(_activate_prepared)
|
|
_parent_node().add_child(_bridge)
|
|
_bridge.start()
|
|
|
|
|
|
func _present_state(message: String, failed: bool = false, record: bool = true) -> void:
|
|
if failed:
|
|
print("MCP | v3 bridge: %s" % message)
|
|
if record:
|
|
MigrationCoordinator.record_status(message)
|
|
_free_bridge()
|
|
_coordinator = null
|
|
if _label != null:
|
|
_label.text = message
|
|
if _retry != null:
|
|
_retry.visible = failed
|
|
|
|
|
|
func _present_error(message: String) -> void:
|
|
_present_state(message, true)
|
|
|
|
|
|
func _activate_prepared(package: Dictionary) -> void:
|
|
_free_bridge()
|
|
print("MCP | v3 bridge activating canonical v4 tree")
|
|
_coordinator = MigrationCoordinator.new()
|
|
_coordinator.state_changed.connect(_present_state)
|
|
_parent_node().add_child(_coordinator)
|
|
_coordinator.start(package)
|
|
|
|
|
|
func _on_retry() -> void:
|
|
if not _meets_floor():
|
|
_present_state(FLOOR_MESSAGE, true, false)
|
|
return
|
|
if not _editor_available():
|
|
_present_state(EDITOR_REQUIRED, true, false)
|
|
return
|
|
_start_migration()
|
|
|
|
|
|
func _free_bridge() -> void:
|
|
if _bridge != null and is_instance_valid(_bridge):
|
|
_bridge.queue_free()
|
|
_bridge = null
|
|
|
|
|
|
func _parent_node() -> Node:
|
|
var parent: Node = EditorInterface.get_base_control()
|
|
return parent if parent != null else get_tree().root
|
|
|
|
|
|
static func _meets_floor() -> bool:
|
|
## Test-only: lets a scenario exercise the refusal path on a current Godot.
|
|
if OS.get_environment("GODOT_AI_TEST_GODOT_FLOOR") == "unmet":
|
|
return false
|
|
var version := Engine.get_version_info()
|
|
return int(version.get("major", 0)) == 4 and int(version.get("minor", 0)) >= 7
|
|
|
|
|
|
## Updates are interactive-only: a headless import or an export run loads
|
|
## enabled plugins too, and must never swap the add-on tree or restart.
|
|
static func _editor_available() -> bool:
|
|
if not Engine.is_editor_hint():
|
|
return false
|
|
if _truthy(OS.get_environment("GODOT_AI_ALLOW_HEADLESS")):
|
|
## The override the v4 plugin honours too, so CI can drive the
|
|
## crossing in a headless editor. Never set it for a real install.
|
|
return EditorInterface.get_base_control() != null
|
|
if DisplayServer.get_name().to_lower() == "headless":
|
|
return false
|
|
if _args_request_headless(OS.get_cmdline_args()):
|
|
return false
|
|
return EditorInterface.get_base_control() != null
|
|
|
|
|
|
static func _truthy(value: String) -> bool:
|
|
var lowered := value.strip_edges().to_lower()
|
|
return lowered == "1" or lowered == "true" or lowered == "yes" or lowered == "on"
|
|
|
|
|
|
static func _args_request_headless(args: PackedStringArray) -> bool:
|
|
for i in range(args.size()):
|
|
var arg := args[i]
|
|
if arg == "--headless":
|
|
return true
|
|
if arg == "--display-driver" and i + 1 < args.size() and args[i + 1] == "headless":
|
|
return true
|
|
if arg.begins_with("--display-driver=") and arg.get_slice("=", 1) == "headless":
|
|
return true
|
|
return false
|
|
|
|
|
|
static func _bridge_status() -> Dictionary:
|
|
var settings := EditorInterface.get_editor_settings()
|
|
var key := MigrationCoordinator.status_setting()
|
|
if settings == null or not settings.has_setting(key):
|
|
return {}
|
|
var parsed: Variant = JSON.parse_string(str(settings.get_setting(key)))
|
|
return parsed if parsed is Dictionary else {}
|
|
|
|
|
|
static func _clear_bridge_status() -> void:
|
|
var settings := EditorInterface.get_editor_settings()
|
|
var key := MigrationCoordinator.status_setting()
|
|
if settings != null and settings.has_setting(key):
|
|
settings.erase(key)
|