godot ai插件
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
@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)
|
||||
Reference in New Issue
Block a user