更新godot ai到最新版本
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
@tool
|
||||
extends Node
|
||||
|
||||
## Puts the final v3 add-on back when this Godot is below v4's floor. The
|
||||
## user's own updater already overlaid the capsule over their v3 tree and
|
||||
## discarded its per-file backups, so without this the plugin stays dead
|
||||
## until they upgrade Godot. Parented outside the EditorPlugin so it survives
|
||||
## the capsule being disabled. Leaves with the capsule once the fleet is on
|
||||
## v4 (docs/self-update.md).
|
||||
|
||||
const PLUGIN_CFG := "res://addons/godot_ai/plugin.cfg"
|
||||
const LIVE_ROOT := "res://addons/godot_ai"
|
||||
const FALLBACK_ZIP := "res://addons/godot_ai/migration_payload/godot-ai-v3-plugin.zip"
|
||||
const LIVE_PREFIX := "addons/godot_ai/"
|
||||
## Every file is written and read back here before the live tree is touched.
|
||||
## The update directory is hidden from Godot's scanner and from git exactly
|
||||
## as utils/update_installer.gd hides it.
|
||||
const UPDATE_ROOT := "res://addons/.godot_ai_update"
|
||||
const STAGE_ROOT := UPDATE_ROOT + "/v3-restore"
|
||||
const UPDATE_IGNORE_FILES := [[".gdignore", ""], [".gitignore", "*\n"]]
|
||||
const MAX_FILES := 4096
|
||||
const MAX_TOTAL_BYTES := 64 * 1024 * 1024
|
||||
## Capsule-only files; the restored v3 tree must not carry them.
|
||||
const CAPSULE_ONLY := [
|
||||
"migration_bridge.gd", "migration_bridge.gd.uid",
|
||||
"migration_coordinator.gd", "migration_coordinator.gd.uid",
|
||||
"migration_fallback.gd", "migration_fallback.gd.uid",
|
||||
"utils/release_verifier.gd", "utils/release_verifier.gd.uid",
|
||||
"utils/update_installer.gd", "utils/update_installer.gd.uid",
|
||||
"migration_payload",
|
||||
]
|
||||
|
||||
|
||||
static func available() -> bool:
|
||||
return FileAccess.file_exists(FALLBACK_ZIP)
|
||||
|
||||
|
||||
func start() -> void:
|
||||
_restore.call_deferred()
|
||||
|
||||
|
||||
func _restore() -> void:
|
||||
var entries: Array = []
|
||||
var error := _read_payload(entries)
|
||||
if error.is_empty():
|
||||
error = _prepare_update_root()
|
||||
if error.is_empty():
|
||||
error = _write_all(STAGE_ROOT, entries)
|
||||
if error.is_empty():
|
||||
error = _write_all(LIVE_ROOT, entries)
|
||||
if not error.is_empty():
|
||||
## The capsule payload and any stage stay for a retry; nothing is
|
||||
## removed until the restored plugin has been enabled.
|
||||
push_error("Godot AI v4 migration: could not restore the previous version: %s" % error)
|
||||
queue_free()
|
||||
return
|
||||
print(
|
||||
"MCP | v3 bridge restored Godot AI v%s; update again on Godot 4.7 or newer"
|
||||
% _restored_version()
|
||||
)
|
||||
## Disabling the capsule removed its dock; re-enabling after a scan loads
|
||||
## the restored plugin.gd, exactly as final v3's own updater re-enables.
|
||||
EditorInterface.set_plugin_enabled(PLUGIN_CFG, false)
|
||||
var filesystem := EditorInterface.get_resource_filesystem()
|
||||
if filesystem == null:
|
||||
_enable_after_scan.call_deferred()
|
||||
return
|
||||
filesystem.filesystem_changed.connect(_enable_after_scan, CONNECT_ONE_SHOT)
|
||||
filesystem.scan()
|
||||
|
||||
|
||||
func _enable_after_scan() -> void:
|
||||
EditorInterface.set_plugin_enabled(PLUGIN_CFG, true)
|
||||
if not EditorInterface.is_plugin_enabled(PLUGIN_CFG):
|
||||
push_error(
|
||||
"Godot AI v4 migration: the restored plugin could not be enabled;"
|
||||
+ " the capsule payload is kept for another attempt"
|
||||
)
|
||||
queue_free()
|
||||
return
|
||||
## Only a running restored plugin makes the capsule material disposable.
|
||||
_remove_capsule_only()
|
||||
_discard_stage()
|
||||
queue_free()
|
||||
|
||||
|
||||
## Every entry must be a regular file under the live add-on path: no absolute
|
||||
## paths, no `..`, nothing outside `addons/godot_ai/`. Read everything before
|
||||
## writing anything, so a bad archive changes nothing.
|
||||
func _read_payload(entries: Array) -> String:
|
||||
var reader := ZIPReader.new()
|
||||
if reader.open(FALLBACK_ZIP) != OK:
|
||||
return "cannot open %s" % FALLBACK_ZIP
|
||||
var files := reader.get_files()
|
||||
if files.size() > MAX_FILES:
|
||||
reader.close()
|
||||
return "too many entries"
|
||||
var total := 0
|
||||
for name in files:
|
||||
if name.ends_with("/"):
|
||||
continue
|
||||
var segments := name.split("/")
|
||||
if (
|
||||
not name.begins_with(LIVE_PREFIX)
|
||||
or name.contains("\\")
|
||||
or segments.has("..")
|
||||
or segments.has(".")
|
||||
or segments.has("")
|
||||
):
|
||||
reader.close()
|
||||
return "unsafe entry %s" % name
|
||||
var data := reader.read_file(name)
|
||||
total += data.size()
|
||||
if total > MAX_TOTAL_BYTES:
|
||||
reader.close()
|
||||
return "payload too large"
|
||||
entries.append([name, data])
|
||||
reader.close()
|
||||
if entries.is_empty():
|
||||
return "empty archive"
|
||||
return ""
|
||||
|
||||
|
||||
static func _prepare_update_root() -> String:
|
||||
var root := ProjectSettings.globalize_path(UPDATE_ROOT)
|
||||
if DirAccess.make_dir_recursive_absolute(root) != OK:
|
||||
return "cannot create %s" % root
|
||||
for marker in UPDATE_IGNORE_FILES:
|
||||
var path := root.path_join(str(marker[0]))
|
||||
if FileAccess.file_exists(path):
|
||||
continue
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return "cannot write %s" % path
|
||||
file.store_string(str(marker[1]))
|
||||
file.close()
|
||||
_remove_dir(ProjectSettings.globalize_path(STAGE_ROOT))
|
||||
return ""
|
||||
|
||||
|
||||
## Drop the stage; when nothing but the ignore markers is left, drop the
|
||||
## update directory too, so a refused or restored crossing leaves no trace.
|
||||
static func _discard_stage() -> void:
|
||||
_remove_dir(ProjectSettings.globalize_path(STAGE_ROOT))
|
||||
var root := ProjectSettings.globalize_path(UPDATE_ROOT)
|
||||
var dir := DirAccess.open(root)
|
||||
if dir == null:
|
||||
return
|
||||
dir.include_hidden = true
|
||||
dir.include_navigational = false
|
||||
if not dir.get_directories().is_empty():
|
||||
return
|
||||
var markers := PackedStringArray()
|
||||
for marker in UPDATE_IGNORE_FILES:
|
||||
markers.append(str(marker[0]))
|
||||
for name in dir.get_files():
|
||||
if not markers.has(name):
|
||||
return
|
||||
_remove_dir(root)
|
||||
|
||||
|
||||
## Write every entry under `root`, checking each write and reading it back,
|
||||
## so a full disk or a refused file stops before anything else is touched.
|
||||
static func _write_all(root: String, entries: Array) -> String:
|
||||
for entry in entries:
|
||||
var relative: String = str(entry[0]).trim_prefix(LIVE_PREFIX)
|
||||
var error := _write_checked(root + "/" + relative, entry[1])
|
||||
if not error.is_empty():
|
||||
return error
|
||||
return ""
|
||||
|
||||
|
||||
static func _write_checked(target: String, data: PackedByteArray) -> String:
|
||||
var parent := ProjectSettings.globalize_path(target.get_base_dir())
|
||||
if DirAccess.make_dir_recursive_absolute(parent) != OK:
|
||||
return "cannot create %s" % parent
|
||||
var file := FileAccess.open(target, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return "cannot write %s (%s)" % [target, error_string(FileAccess.get_open_error())]
|
||||
file.store_buffer(data)
|
||||
var error := file.get_error()
|
||||
file.close()
|
||||
if error != OK:
|
||||
return "write failed for %s (%s)" % [target, error_string(error)]
|
||||
var written := FileAccess.get_file_as_bytes(target)
|
||||
if written.size() != data.size():
|
||||
return "short write for %s (%d of %d bytes)" % [target, written.size(), data.size()]
|
||||
return ""
|
||||
|
||||
|
||||
func _remove_capsule_only() -> void:
|
||||
for relative in CAPSULE_ONLY:
|
||||
var path := ProjectSettings.globalize_path(LIVE_ROOT + "/" + str(relative))
|
||||
if DirAccess.dir_exists_absolute(path):
|
||||
_remove_dir(path)
|
||||
elif FileAccess.file_exists(path):
|
||||
DirAccess.remove_absolute(path)
|
||||
|
||||
|
||||
static func _remove_dir(path: String) -> void:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
return
|
||||
dir.include_hidden = true
|
||||
dir.include_navigational = false
|
||||
for name in dir.get_directories():
|
||||
_remove_dir(path.path_join(name))
|
||||
for name in dir.get_files():
|
||||
DirAccess.remove_absolute(path.path_join(name))
|
||||
DirAccess.remove_absolute(path)
|
||||
|
||||
|
||||
static func _restored_version() -> String:
|
||||
var config := ConfigFile.new()
|
||||
if config.load(PLUGIN_CFG) != OK:
|
||||
return "?"
|
||||
return str(config.get_value("plugin", "version", "?"))
|
||||
Reference in New Issue
Block a user