再次合并最新master到策划Agent模型设置分支
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 20s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 13s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 20s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 20s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 20s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / AI game creator shell web tests (pull_request) Failing after 13s
Project CI / Repository checks (pull_request) Failing after 13s
同步远端master最新提交并保留策划Agent模型设置改动
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
## 2026-09-20 Godot 编辑器执行接入
|
||||
|
||||
原生引导采用固定版本的官方 `godot-cpp` 和 MSVC x64 构建,绑定及 C++ runtime 静态链接。依赖归档和缓存源码须核验,安装目录仍只分发原生载荷及许可。EDITOR 阶段动态加载/卸载时显式清理 C++ 实例绑定与单例包装,保留纯 GDScript 的异步执行和原有协议;执行权限、项目身份与缓存归属继续由现有宿主处理。
|
||||
|
||||
可用性边界按引擎区分:Cocos/Unity 保持不按工程类型过滤,Godot 仍绑定当前 Godot 项目,切项目撤销旧插件上下文;前端统一根据宿主投影启动插件。Runtime 工具目录只对 Godot 追加项目条件,编辑器说明沿用外置提示词及审核 Skill 参考。
|
||||
|
||||
Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和不确定执行回执合同,编辑器实现留在 `plugins/agc-godot-editor`。用户选择 DLL 原件随 AGC 安装资源分发,并确认按编辑器实例在 AGC 私有缓存准备临时加载副本,以满足 Godot Windows 加载器的同目录 `~DLL` 写入要求;项目内不复制 DLL,只用受管 `.gdextension` 引导。Godot 自动 UID 伴生文件必须记录归属并在确认卸载后按内容匹配清理。工作区根不迁移到 Godot 子目录,原始项目配置与场景只通过明确编辑操作修改。完整合同及验证范围见 [Godot 编辑器插件接入](<../../technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md>)。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## Godot 原生插件在 MSVC C 模式下的对齐声明
|
||||
## Godot C++ 扩展构建与对象生命周期
|
||||
|
||||
- `native.c` 若首先报 `max_align_t` 语法错误,后面的 `Storage`、`retained_script` 等未声明通常是连带错误。MSVC C 模式不提供该类型;ABI 存储使用 C11 `_Alignas(16)` 显式对齐并保持 128 字节容量,通过实际 MSVC DLL 构建验证,不逐条修补连带错误。
|
||||
- `No C compiler found` 则属于开发环境问题:先初始化 Visual Studio x64 开发环境,同时设置 PATH、INCLUDE 与 LIB,再运行构建。只把 `cl.exe` 所在目录加到 PATH 不足以提供头文件和链接库。
|
||||
- 原生引导通过官方 `godot-cpp` 管理 Variant、String 和 Ref,不自行维护 ABI 存储。Godot 类型必须在扩展终止回调内释放,不能依赖 DLL 静态对象析构;桥节点可能已经退出,应按实例 ID 核验存活再回调。
|
||||
- 正式 Windows 构建使用 CMake 的 Visual Studio x64 generator,并实际验证 MSVC 编译;不能用 GCC 成功替代 MSVC 验收。固定官方归档按 SHA256 校验,缓存源码被修改时拒绝构建并保留证据。
|
||||
|
||||
## Rust 同步回调的测试记录按线程隔离
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@ DLL 原件随 AGC 安装包放在插件资源目录中;Godot Windows 加载器
|
||||
|
||||
## 分发与描述文件
|
||||
|
||||
- Windows 原生构建使用 x64 C11 编译器;MSVC 需先初始化 Visual Studio 的 x64 开发环境,以同时提供 PATH、INCLUDE 与 LIB。ABI 临时存储使用 128 字节、C11 `_Alignas(16)` 显式对齐,不依赖 MSVC C 模式未提供的 `max_align_t`;定向验证运行 `plugins/agc-godot-editor/native/gdextension/build.ps1 -Compiler cl.exe`。
|
||||
- 原生引导使用官方 `godot-cpp` 的 C++ 类型和初始化接口,不自行声明 Variant 存储或直接装配 ABI 函数指针。绑定源码固定到 `godot-4.5-stable` 的 `e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77`,以该版本的稳定 API 构建并在 Godot 4.7.2 验证;产品最低版本仍为 4.7,因为嵌入脚本使用该版本能力。Windows x64 构建使用 Visual Studio C++、CMake 和 Python,静态链接绑定及 C++ runtime,用户无需这些构建工具。
|
||||
- 构建仅从已固定的官方归档获取绑定源码,并核对 SHA256;下载和生成内容只进入 `.build/`。CMake 构建包含引导源码、嵌入脚本、绑定版本/归档摘要和构建配置的身份指纹。许可证与来源继续随 DLL 分发,安装包不包含 SDK、源码缓存、生成绑定或构建工具。
|
||||
- C++ 状态仅在扩展有效期间持有 GDScript 引用和桥节点身份;终止回调先停用仍存活的桥,再释放绑定对象,不能让静态 Godot 对象析构晚于绑定退出。延迟 bootstrap、原生卸载、Node 已退出及同 PID 重连均须实测。受管文件、会话身份、协议、执行回执与不确定阻断沿用现有合同。
|
||||
- EDITOR 阶段晚加载不会取得 CORE 阶段终止回调;引导终止时须通过官方绑定接口解除 Node/GDScript 的实例包装回调,并完成单例包装清理。只移除 C++ 包装,不同步销毁仍在 GDScript 调用栈或等待 `queue_free` 的引擎对象;先用 Variant 保活脚本,再解除 Ref 和绑定,避免卸载 DLL 后跳到失效回调。
|
||||
|
||||
- 安装资源布局为 `plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll`,邻接元数据记录协议、构建身份和 DLL SHA256。开发模式允许宿主提供仓库插件目录中的同结构产物;RPC 不接受自定义 DLL 候选。
|
||||
- 缓存根仅由宿主提供,为其私有配置目录下的 `godot-editor-runtime`;按 `PID + startedFileTime + buildId` 隔离,所有路径分量受控且拒绝链接/reparse point。复制前验证安装原件及元数据,缓存已有文件必须匹配来源、归属及 SHA,不能加载被替换的同名文件。受管描述同时保留原件与加载副本身份,重启恢复不得把工程给出的任意 DLL 路径当作受信任来源。
|
||||
@@ -107,11 +110,13 @@ Windows x64、Godot 4.7.2 标准编辑器的本地实现验收通过。证据保
|
||||
|
||||
### 复验入口
|
||||
|
||||
从仓库根运行,原生构建要求 Windows x64 C 编译器。先把 `AGC_GODOT_TEST_EXECUTABLE` 设置为待验证的标准 Godot 编辑器绝对路径;未设置时 headless 测试会跳过,不能视为实机通过。
|
||||
从仓库根运行,原生构建要求 Visual Studio 2022 C++ x64、CMake 3.25 及以上和 Python 3;通过 Visual Studio generator 自动选择完整编译环境。首次构建需访问固定的官方归档,校验后的依赖缓存支持离线复用;`build.ps1` 可用 `-CMake`、`-Python` 指定构建工具。先把 `AGC_GODOT_TEST_EXECUTABLE` 设置为待验证的标准 Godot 编辑器绝对路径;未设置时 headless 测试会跳过,不能视为实机通过。
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -File plugins/agc-godot-editor/native/gdextension/build.ps1
|
||||
python -X utf8 -B plugins/agc-godot-editor/native/gdextension/tests/test_dependencies.py
|
||||
node --test plugins/agc-godot-editor/native/gdextension/tests/native-smoke.test.mjs
|
||||
node --test plugins/agc-godot-editor/native/gdextension/tests/guide-examples.test.mjs
|
||||
cargo test --locked --manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml
|
||||
npm run agc:plugins:test
|
||||
cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features cocos-editor-execute,unity-editor-execute,godot-editor-execute godot
|
||||
@@ -124,3 +129,9 @@ git diff --check
|
||||
```
|
||||
|
||||
真实 GUI 使用 `native/godot-editor-bridge/examples/live_smoke.rs`;安装位置变更使用同目录的 `install_location_smoke.rs`。二者要求显式传入自有可丢弃工程、已打开编辑器 PID、可信安装 DLL、工程外私有缓存及 `--allow-fixture-mutations`,具体参数见源码用法。多实例验证分别传入两个工程和 PID,通过 `live_smoke` 的 `--hold-ms` 让加载时间重叠,同时核验原生模块路径。Runner 复验须经正式长度前缀 RPC、完整 ACK 和私有配置恢复路径,不能用原生示例替代 Runner 证据。
|
||||
|
||||
### C++ 引导验收边界
|
||||
|
||||
官方 C++ 绑定版已通过实际 MSVC 构建、原生/指南 headless 回归、Rust 缓存与连接回归、受管资源 staging 校验,以及同一 Godot 4.7.2 GUI 进程中的首次加载、执行、卸载和重新聚焦后的重连。DLL 的导入依赖只有 KERNEL32,生成 SDK/编译缓存不进入 staging;重复构建命中同一载荷身份,损坏归档和改动过的依赖缓存拒绝构建。验证中首次创建的空工程由 Godot 补写版本特征,按编辑器初始化后的基线确认插件运行不修改原有工程文件。
|
||||
|
||||
GUI 自动聚焦若未触发扫描,会按原合同返回未派发错误;重新聚焦后再连接,不重放未知执行。此次 C++ 替换未重新运行完整 AGC 发布构建、真实 Provider 或安装包 UI 验证,前述旧版本证据不代替这些验收。
|
||||
|
||||
+2
-1
@@ -83,7 +83,8 @@ feature 会构建自包含 Attach helper,并只将运行文件与许可放入
|
||||
[Unity 插件接入](../docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md)。
|
||||
|
||||
Godot 插件通过 `godot-editor-execute` feature 构建并校验 GDExtension 载荷,只分发
|
||||
运行入口、DLL、元数据及许可。构建机需要 Windows x64 C 工具链;DLL 原件留在安装资源,
|
||||
运行入口、DLL、元数据及许可。原生引导使用固定版本的官方 `godot-cpp`;构建机需要
|
||||
Visual Studio C++ x64、CMake 和 Python,首次构建下载并校验绑定源码。DLL 原件留在安装资源,
|
||||
每个编辑器的临时加载副本放在 AGC 私有缓存。连接时维护工程内受管 `.gdextension`
|
||||
引用及其 UID,通过 Godot 聚焦扫描首次加载。文件归属、真实执行和卸载规则见
|
||||
[Godot 插件接入](<../docs/technical/【技术方案】AGC Godot编辑器插件接入-2026-09-20.md>)。
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(agc_godot_editor LANGUAGES CXX)
|
||||
if(NOT MSVC OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "Godot editor payload requires MSVC x64")
|
||||
endif()
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded CACHE STRING "Static runtime" FORCE)
|
||||
set(GODOTCPP_TARGET editor CACHE STRING "Editor bindings" FORCE)
|
||||
set(GODOTCPP_BUILD_PROFILE "${CMAKE_CURRENT_SOURCE_DIR}/build-profile.json" CACHE FILEPATH "Minimal bindings" FORCE)
|
||||
set(GODOTCPP_USE_STATIC_CPP ON CACHE BOOL "Static runtime" FORCE)
|
||||
set(GODOTCPP_USE_HOT_RELOAD OFF CACHE BOOL "Explicit unload/reload only" FORCE)
|
||||
set(GODOTCPP_ENABLE_TESTING OFF CACHE BOOL "Do not package upstream tests" FORCE)
|
||||
if(NOT EXISTS "${AGC_GODOT_CPP_SOURCE}/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Run build.ps1 to prepare the verified godot-cpp source")
|
||||
endif()
|
||||
add_subdirectory("${AGC_GODOT_CPP_SOURCE}" godot-cpp EXCLUDE_FROM_ALL SYSTEM)
|
||||
add_library(agc_godot_editor SHARED src/native.cpp)
|
||||
target_compile_features(agc_godot_editor PRIVATE cxx_std_17)
|
||||
target_link_libraries(agc_godot_editor PRIVATE godot::cpp)
|
||||
target_include_directories(agc_godot_editor PRIVATE "${AGC_GENERATED_DIR}")
|
||||
target_compile_options(agc_godot_editor PRIVATE /W4 /WX /utf-8)
|
||||
set_target_properties(agc_godot_editor PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/payload")
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"enabled_classes": ["GDScript", "Node", "OS", "ProjectSettings"]
|
||||
}
|
||||
@@ -1,84 +1,73 @@
|
||||
param([string]$Compiler = $env:AGC_GODOT_C_COMPILER)
|
||||
param(
|
||||
[string]$CMake = 'cmake.exe',
|
||||
[string]$Python = 'python.exe',
|
||||
[string]$Generator = 'Visual Studio 17 2022',
|
||||
[ValidateRange(1, 32)][int]$Jobs = 4
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = $PSScriptRoot
|
||||
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Godot editor native payload requires Windows x64.' }
|
||||
if (-not [Environment]::Is64BitProcess) { throw 'A 64-bit build host is required.' }
|
||||
if (-not $Compiler) {
|
||||
foreach ($candidate in @('gcc.exe', 'clang.exe', 'cl.exe')) {
|
||||
$found = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||
if ($found) { $Compiler = $found.Source; break }
|
||||
}
|
||||
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess) {
|
||||
throw 'Godot editor payload requires Windows x64.'
|
||||
}
|
||||
if (-not $Compiler) { throw 'No C compiler found. Install a Windows x64 C toolchain or pass -Compiler.' }
|
||||
$Compiler = (Get-Command $Compiler -ErrorAction Stop).Source
|
||||
$build = Join-Path $root '.build'
|
||||
$CMake = (Get-Command $CMake -ErrorAction Stop).Source
|
||||
$Python = (Get-Command $Python -ErrorAction Stop).Source
|
||||
$build = Join-Path $root '.build/msvc'
|
||||
$generated = Join-Path $root '.build/generated'
|
||||
$output = Join-Path $root 'bin/win-x64'
|
||||
New-Item -ItemType Directory -Path $build,$output -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $generated,$output -Force | Out-Null
|
||||
$utf8 = [Text.UTF8Encoding]::new($false)
|
||||
$inputs = @('src/native.c','src/bridge.gd','vendor/gdextension_interface.h','vendor/provenance.json','build.ps1')
|
||||
$fingerprint = 'agc.godot.editor.v1/windows/x86_64/c11/O2' + "`n"
|
||||
$fingerprint += 'compiler:' + (Get-FileHash -Algorithm SHA256 -LiteralPath $Compiler).Hash.ToLowerInvariant() + "`n"
|
||||
foreach ($inputPath in $inputs) { $fingerprint += $inputPath + ':' + (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $root $inputPath)).Hash.ToLowerInvariant() + "`n" }
|
||||
$hasher = [Security.Cryptography.SHA256]::Create()
|
||||
try { $buildId = 'sha256:' + ([BitConverter]::ToString($hasher.ComputeHash($utf8.GetBytes($fingerprint))).Replace('-','').ToLowerInvariant()) } finally { $hasher.Dispose() }
|
||||
$existingDll = Join-Path $output 'agc_godot_editor.dll'
|
||||
$existingMetadata = Join-Path $output 'metadata.json'
|
||||
if ((Test-Path -LiteralPath $existingDll) -and (Test-Path -LiteralPath $existingMetadata)) {
|
||||
try {
|
||||
$existing = Get-Content -LiteralPath $existingMetadata -Raw | ConvertFrom-Json
|
||||
$existingHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $existingDll).Hash.ToLowerInvariant()
|
||||
if ($existing.protocol -eq 'agc.godot.editor.v1' -and $existing.buildId -eq $buildId -and $existing.sha256 -eq $existingHash -and $existing.entrySymbol -eq 'agc_godot_editor_init' -and $existing.platform -eq 'windows' -and $existing.arch -eq 'x86_64' -and $existing.minimumGodotVersion -eq '4.7') {
|
||||
Write-Output "Native payload is current: $buildId"
|
||||
$previousBytecode = $env:PYTHONDONTWRITEBYTECODE
|
||||
try {
|
||||
$env:PYTHONDONTWRITEBYTECODE = '1'
|
||||
$dependency = & $Python -X utf8 (Join-Path $root 'prepare_dependencies.py')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Pinned godot-cpp dependency verification failed.' }
|
||||
& $CMake -S $root -B $build -G $Generator -A x64 "-DAGC_GODOT_CPP_SOURCE=$dependency" "-DAGC_GENERATED_DIR=$generated" "-DPython3_EXECUTABLE=$Python"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Godot C++ configure failed; install Visual Studio C++ x64, CMake and Python.' }
|
||||
$compilerRecords = @(Get-ChildItem -LiteralPath (Join-Path $build 'CMakeFiles') -Filter 'CMakeCXXCompiler.cmake' -Recurse -File)
|
||||
if ($compilerRecords.Count -ne 1) { throw 'MSVC compiler identity is ambiguous.' }
|
||||
$record = [IO.File]::ReadAllText($compilerRecords[0].FullName)
|
||||
$compilerMatch = [regex]::Match($record, 'set\(CMAKE_CXX_COMPILER "([^"]+)"\)')
|
||||
if (-not $compilerMatch.Success -or -not $record.Contains('set(CMAKE_CXX_COMPILER_ID "MSVC")')) { throw 'MSVC compiler identity was not verified.' }
|
||||
$compiler = $compilerMatch.Groups[1].Value
|
||||
$inputs = @('src/native.cpp','src/bridge.gd','CMakeLists.txt','build-profile.json','prepare_dependencies.py','vendor/provenance.json','vendor/LICENSE.txt','build.ps1')
|
||||
$fingerprint = 'agc.godot.editor.v1/windows/x86_64/c++17/msvc/Release/static-crt' + "`n"
|
||||
$fingerprint += 'compiler:' + (Get-FileHash -LiteralPath $compiler -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
|
||||
$fingerprint += 'cmake:' + (Get-FileHash -LiteralPath $CMake -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
|
||||
$fingerprint += 'configuration:' + (Get-FileHash -LiteralPath (Join-Path $build 'CMakeCache.txt') -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
|
||||
$fingerprint += 'toolchain:' + (Get-FileHash -LiteralPath $compilerRecords[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "`n"
|
||||
foreach ($relative in $inputs) { $fingerprint += $relative + ':' + (Get-FileHash -LiteralPath (Join-Path $root $relative) -Algorithm SHA256).Hash.ToLowerInvariant() + "`n" }
|
||||
$hasher = [Security.Cryptography.SHA256]::Create()
|
||||
try { $buildId = 'sha256:' + ([BitConverter]::ToString($hasher.ComputeHash($utf8.GetBytes($fingerprint))).Replace('-','').ToLowerInvariant()) } finally { $hasher.Dispose() }
|
||||
$dll = Join-Path $output 'agc_godot_editor.dll'
|
||||
$metadataPath = Join-Path $output 'metadata.json'
|
||||
if ((Test-Path -LiteralPath $dll) -and (Test-Path -LiteralPath $metadataPath)) {
|
||||
$existing = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json
|
||||
if ($existing.protocol -eq 'agc.godot.editor.v1' -and $existing.entrySymbol -eq 'agc_godot_editor_init' -and $existing.platform -eq 'windows' -and $existing.arch -eq 'x86_64' -and $existing.minimumGodotVersion -eq '4.7' -and $existing.buildId -eq $buildId -and $existing.sha256 -eq (Get-FileHash -LiteralPath $dll -Algorithm SHA256).Hash.ToLowerInvariant()) {
|
||||
Write-Output "Native C++ payload is current: $buildId"
|
||||
return
|
||||
}
|
||||
} catch { Write-Verbose 'Existing metadata could not be verified; rebuilding.' }
|
||||
}
|
||||
$script = [IO.File]::ReadAllBytes((Join-Path $root 'src/bridge.gd'))
|
||||
$embedded = [Text.StringBuilder]::new()
|
||||
[void]$embedded.AppendLine('/* Generated from src/bridge.gd; never reads a project-side script. */')
|
||||
[void]$embedded.AppendLine('#define AGC_BUILD_ID "' + $buildId + '"')
|
||||
[void]$embedded.AppendLine('static const unsigned char AGC_EMBEDDED_BRIDGE[] = {')
|
||||
for ($index = 0; $index -lt $script.Length; $index += 32) {
|
||||
$last = [Math]::Min($index + 31, $script.Length - 1)
|
||||
[void]$embedded.AppendLine(($script[$index..$last] -join ',') + ',')
|
||||
}
|
||||
[void]$embedded.AppendLine('0};')
|
||||
[IO.File]::WriteAllText((Join-Path $build 'embedded_bridge.h'), $embedded.ToString(), $utf8)
|
||||
$previousTemp = $env:TEMP
|
||||
$previousTmp = $env:TMP
|
||||
$previousLocation = Get-Location
|
||||
try {
|
||||
$env:TEMP = $build
|
||||
$env:TMP = $build
|
||||
Set-Location -LiteralPath $build
|
||||
$source = Join-Path $root 'src/native.c'
|
||||
$vendor = Join-Path $root 'vendor'
|
||||
$temporaryDll = Join-Path $build 'agc_godot_editor.dll'
|
||||
$compilerName = [IO.Path]::GetFileName($Compiler).ToLowerInvariant()
|
||||
if ($compilerName -eq 'cl.exe') {
|
||||
& $Compiler /nologo /std:c11 /O2 /W4 /WX /LD /D_CRT_SECURE_NO_WARNINGS "/I$vendor" "/I$build" $source "/Fe:$temporaryDll" /link /Brepro
|
||||
} else {
|
||||
$flags = @('-std=c11','-O2','-Wall','-Wextra','-Werror','-shared')
|
||||
if ($compilerName -eq 'gcc.exe') { $flags += @('-static-libgcc','-Wl,--no-insert-timestamp') }
|
||||
& $Compiler @flags -I $vendor -I $build $source -o $temporaryDll
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { throw "Native compiler exited with $LASTEXITCODE" }
|
||||
$dll = Join-Path $output 'agc_godot_editor.dll'
|
||||
Copy-Item -LiteralPath $temporaryDll -Destination $dll -Force
|
||||
$script = [IO.File]::ReadAllBytes((Join-Path $root 'src/bridge.gd'))
|
||||
$embedded = [Text.StringBuilder]::new()
|
||||
[void]$embedded.AppendLine('#define AGC_BUILD_ID "' + $buildId + '"')
|
||||
[void]$embedded.AppendLine('static const unsigned char AGC_EMBEDDED_BRIDGE[] = {')
|
||||
for ($index = 0; $index -lt $script.Length; $index += 32) {
|
||||
$last = [Math]::Min($index + 31, $script.Length - 1)
|
||||
[void]$embedded.AppendLine(($script[$index..$last] -join ',') + ',')
|
||||
}
|
||||
[void]$embedded.AppendLine('0};')
|
||||
$header = Join-Path $generated 'embedded_bridge.h'
|
||||
if (-not (Test-Path -LiteralPath $header) -or [IO.File]::ReadAllText($header) -ne $embedded.ToString()) { [IO.File]::WriteAllText($header, $embedded.ToString(), $utf8) }
|
||||
& $CMake --build $build --config Release --target agc_godot_editor --parallel $Jobs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Godot C++ MSVC build failed.' }
|
||||
Copy-Item -LiteralPath (Join-Path $build 'payload/agc_godot_editor.dll') -Destination $dll -Force
|
||||
$metadata = [ordered]@{
|
||||
protocol = 'agc.godot.editor.v1'
|
||||
buildId = $buildId
|
||||
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $dll).Hash.ToLowerInvariant()
|
||||
platform = 'windows'
|
||||
arch = 'x86_64'
|
||||
entrySymbol = 'agc_godot_editor_init'
|
||||
minimumGodotVersion = '4.7'
|
||||
protocol = 'agc.godot.editor.v1'; buildId = $buildId
|
||||
sha256 = (Get-FileHash -LiteralPath $dll -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
platform = 'windows'; arch = 'x86_64'; entrySymbol = 'agc_godot_editor_init'; minimumGodotVersion = '4.7'
|
||||
}
|
||||
[IO.File]::WriteAllText((Join-Path $output 'metadata.json'), ($metadata | ConvertTo-Json) + "`n", $utf8)
|
||||
Write-Output "Built $dll"
|
||||
Write-Output "Build identity: $buildId"
|
||||
[IO.File]::WriteAllText($metadataPath, ($metadata | ConvertTo-Json) + "`n", $utf8)
|
||||
Write-Output "Built C++ payload: $buildId"
|
||||
} finally {
|
||||
Set-Location -LiteralPath $previousLocation
|
||||
$env:TEMP = $previousTemp
|
||||
$env:TMP = $previousTmp
|
||||
$env:PYTHONDONTWRITEBYTECODE = $previousBytecode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Prepare only the pinned official SDK; never execute an unchecked archive."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
|
||||
def plain(path):
|
||||
for item in (path, *path.parents):
|
||||
if item.exists() or item.is_symlink():
|
||||
info = item.lstat()
|
||||
if item.is_symlink() or getattr(info, "st_file_attributes", 0) & 0x400:
|
||||
raise ValueError(f"Dependency path cannot contain links: {item}")
|
||||
|
||||
|
||||
def digest(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def prepare(root):
|
||||
provenance = json.loads((root / "vendor/provenance.json").read_text(encoding="utf-8"))
|
||||
commit = provenance["commit"]
|
||||
expected = provenance["archiveSha256"]
|
||||
if not re.fullmatch(r"[a-f0-9]{40}", commit) or not re.fullmatch(r"[a-f0-9]{64}", expected):
|
||||
raise ValueError("Invalid pinned dependency identity")
|
||||
url = f"https://codeload.github.com/godotengine/godot-cpp/zip/{commit}"
|
||||
if provenance["archiveUrl"] != url:
|
||||
raise ValueError("Dependency URL must identify the pinned official repository")
|
||||
cache = root / ".build/dependencies"
|
||||
plain(cache)
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
archive = cache / f"{commit}.zip"
|
||||
plain(archive)
|
||||
if not archive.exists():
|
||||
with urllib.request.urlopen(url, timeout=60) as response:
|
||||
data = response.read(32 * 1024 * 1024 + 1)
|
||||
if len(data) > 32 * 1024 * 1024 or digest(data) != expected:
|
||||
raise ValueError("Official godot-cpp archive SHA256 mismatch")
|
||||
with tempfile.NamedTemporaryFile(dir=cache, delete=False) as output:
|
||||
output.write(data)
|
||||
temporary = Path(output.name)
|
||||
os.replace(temporary, archive)
|
||||
if digest(archive.read_bytes()) != expected:
|
||||
raise ValueError("Cached godot-cpp archive SHA256 mismatch; cache was preserved")
|
||||
source = cache / f"godot-cpp-{commit}"
|
||||
plain(source)
|
||||
source.mkdir(exist_ok=True)
|
||||
expected_files = set()
|
||||
with zipfile.ZipFile(archive) as bundle:
|
||||
for entry in bundle.infolist():
|
||||
relative = PurePosixPath(entry.filename)
|
||||
if (not relative.parts or relative.parts[0] != source.name or relative.is_absolute()
|
||||
or ".." in relative.parts or "\\" in entry.filename or ":" in entry.filename):
|
||||
raise ValueError("Unsafe dependency archive entry")
|
||||
if stat.S_ISLNK(entry.external_attr >> 16):
|
||||
raise ValueError("Dependency archive cannot contain symbolic links")
|
||||
if entry.is_dir():
|
||||
continue
|
||||
target = source.joinpath(*relative.parts[1:])
|
||||
if target in expected_files:
|
||||
raise ValueError("Duplicate dependency archive entry")
|
||||
plain(target)
|
||||
expected_files.add(target)
|
||||
content = bundle.read(entry)
|
||||
if target.exists():
|
||||
if not target.is_file() or target.read_bytes() != content:
|
||||
raise ValueError(f"Modified godot-cpp source cache was preserved: {target}")
|
||||
else:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(content)
|
||||
for item in source.rglob("*"):
|
||||
plain(item)
|
||||
if item.is_file() and item not in expected_files:
|
||||
raise ValueError(f"Unexpected dependency source cache entry: {item}")
|
||||
if (source / "LICENSE.md").read_text(encoding="utf-8") != (root / "vendor/LICENSE.txt").read_text(encoding="utf-8"):
|
||||
raise ValueError("Packaged godot-cpp license differs from pinned upstream license")
|
||||
return source
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(prepare(Path(__file__).resolve().parent))
|
||||
except (OSError, ValueError, KeyError, zipfile.BadZipFile) as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -1,258 +0,0 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
#include "gdextension_interface.h"
|
||||
#include "embedded_bridge.h"
|
||||
|
||||
/* Windows x64 ABI storage is deliberately oversized and 16-byte aligned.
|
||||
* Explicit C11 alignment also works with MSVC C, which lacks max_align_t.
|
||||
* Objects are constructed/destructed solely through the official interface. */
|
||||
typedef struct { _Alignas(16) unsigned char bytes[128]; } Storage;
|
||||
static GDExtensionInterfacePrintWarning api_warning;
|
||||
static GDExtensionInterfaceVariantCall api_call;
|
||||
static GDExtensionInterfaceVariantDestroy api_destroy;
|
||||
static GDExtensionInterfaceVariantGetType api_type;
|
||||
static GDExtensionInterfaceGlobalGetSingleton api_singleton;
|
||||
static GDExtensionInterfaceStringNameNewWithLatin1Chars api_name;
|
||||
static GDExtensionInterfaceStringNewWithUtf8Chars api_string;
|
||||
static GDExtensionInterfaceStringToUtf8Chars api_utf8;
|
||||
static GDExtensionVariantFromTypeConstructorFunc from_object, from_string, from_name;
|
||||
static GDExtensionTypeFromVariantConstructorFunc to_int, to_string;
|
||||
static GDExtensionPtrDestructor destroy_name, destroy_string;
|
||||
static Storage retained_script, retained_node;
|
||||
static int script_live, node_live, started;
|
||||
|
||||
static void report_failure(const char *operation, int code) {
|
||||
char message[256];
|
||||
snprintf(message, sizeof(message), "AGC Godot editor bridge: %s failed (%d).", operation, code);
|
||||
if (api_warning) api_warning(message, "agc_godot_editor", "native.c", 0, 0);
|
||||
}
|
||||
|
||||
static void name_variant(Storage *out, const char *text) {
|
||||
Storage name;
|
||||
api_name(&name, text, 0);
|
||||
from_name(out, &name);
|
||||
destroy_name(&name);
|
||||
}
|
||||
|
||||
static void string_variant(Storage *out, const char *text) {
|
||||
Storage string;
|
||||
api_string(&string, text);
|
||||
from_string(out, &string);
|
||||
destroy_string(&string);
|
||||
}
|
||||
|
||||
static int invoke(Storage *receiver, const char *method,
|
||||
const GDExtensionConstVariantPtr *arguments, int count, Storage *out) {
|
||||
Storage name;
|
||||
GDExtensionCallError error = { GDEXTENSION_CALL_OK, 0, 0 };
|
||||
api_name(&name, method, 0);
|
||||
api_call(receiver, &name, arguments, count, out, &error);
|
||||
destroy_name(&name);
|
||||
if (error.error != GDEXTENSION_CALL_OK) {
|
||||
report_failure(method, (int)error.error);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int singleton_variant(Storage *out, const char *text) {
|
||||
Storage name;
|
||||
api_name(&name, text, 0);
|
||||
GDExtensionObjectPtr object = api_singleton(&name);
|
||||
destroy_name(&name);
|
||||
if (!object) return 0;
|
||||
from_object(out, &object);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static char *variant_utf8(Storage *value) {
|
||||
if (api_type(value) != GDEXTENSION_VARIANT_TYPE_STRING) return NULL;
|
||||
Storage string;
|
||||
to_string(&string, value);
|
||||
GDExtensionInt length = api_utf8(&string, NULL, 0);
|
||||
char *text = NULL;
|
||||
if (length >= 0 && length < 131072) {
|
||||
text = (char *)malloc((size_t)length + 1);
|
||||
if (text) {
|
||||
api_utf8(&string, text, length);
|
||||
text[length] = '\0';
|
||||
}
|
||||
}
|
||||
destroy_string(&string);
|
||||
return text;
|
||||
}
|
||||
|
||||
static int plain_directory(const wchar_t *path) {
|
||||
DWORD attrs = GetFileAttributesW(path);
|
||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) &&
|
||||
!(attrs & FILE_ATTRIBUTE_REPARSE_POINT);
|
||||
}
|
||||
|
||||
static int ensure_cache_directory(wchar_t *path, size_t capacity, const wchar_t *part) {
|
||||
size_t length = wcslen(path), addition = wcslen(part);
|
||||
if (length + addition + 2 >= capacity) return 0;
|
||||
if (length && path[length - 1] != L'\\') path[length++] = L'\\';
|
||||
memcpy(path + length, part, (addition + 1) * sizeof(wchar_t));
|
||||
if (!CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) return 0;
|
||||
return plain_directory(path);
|
||||
}
|
||||
|
||||
static char *prepare_cache_path(void) {
|
||||
Storage settings, argument, result;
|
||||
if (!singleton_variant(&settings, "ProjectSettings")) return NULL;
|
||||
string_variant(&argument, "res://");
|
||||
const GDExtensionConstVariantPtr args[] = { &argument };
|
||||
int ok = invoke(&settings, "globalize_path", args, 1, &result);
|
||||
char *root_utf8 = ok ? variant_utf8(&result) : NULL;
|
||||
api_destroy(&result);
|
||||
api_destroy(&argument);
|
||||
api_destroy(&settings);
|
||||
if (!root_utf8) return NULL;
|
||||
wchar_t path[32768];
|
||||
int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, root_utf8, -1, path, 32768);
|
||||
free(root_utf8);
|
||||
if (length < 4 || path[1] != L':') return NULL;
|
||||
for (int index = 0; index < length; ++index) if (path[index] == L'/') path[index] = L'\\';
|
||||
/* Reject links/junctions in every existing directory, including project ancestors. */
|
||||
for (int index = 3; index < length; ++index) {
|
||||
if (path[index] != L'\\' && path[index] != L'\0') continue;
|
||||
wchar_t saved = path[index];
|
||||
path[index] = L'\0';
|
||||
int plain = plain_directory(path);
|
||||
path[index] = saved;
|
||||
if (!plain) return NULL;
|
||||
}
|
||||
if (!ensure_cache_directory(path, 32768, L".godot") ||
|
||||
!ensure_cache_directory(path, 32768, L"agc")) return NULL;
|
||||
wchar_t suffix[96];
|
||||
swprintf(suffix, 96, L"\\editor-bridge-%lu.json", (unsigned long)GetCurrentProcessId());
|
||||
if (wcslen(path) + wcslen(suffix) + 1 >= 32768) return NULL;
|
||||
wcscat(path, suffix);
|
||||
DWORD attrs = GetFileAttributesW(path);
|
||||
if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))) return NULL;
|
||||
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, path, -1, NULL, 0, NULL, NULL);
|
||||
if (size <= 0) return NULL;
|
||||
char *cache = (char *)malloc((size_t)size);
|
||||
if (cache) WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, path, -1, cache, size, NULL, NULL);
|
||||
return cache;
|
||||
}
|
||||
|
||||
static void release_references(void) {
|
||||
if (node_live) { api_destroy(&retained_node); node_live = 0; }
|
||||
if (script_live) { api_destroy(&retained_script); script_live = 0; }
|
||||
}
|
||||
|
||||
static int schedule_bridge(void) {
|
||||
FILETIME creation, exit_time, kernel, user;
|
||||
if (!GetProcessTimes(GetCurrentProcess(), &creation, &exit_time, &kernel, &user)) return 0;
|
||||
ULARGE_INTEGER timestamp;
|
||||
timestamp.LowPart = creation.dwLowDateTime;
|
||||
timestamp.HighPart = creation.dwHighDateTime;
|
||||
char started_file_time[32];
|
||||
snprintf(started_file_time, sizeof(started_file_time), "%llu", (unsigned long long)timestamp.QuadPart);
|
||||
char *cache_path = prepare_cache_path();
|
||||
if (!cache_path) { report_failure("session_cache_path", 0); return 0; }
|
||||
Storage classdb, class_arg, result, source;
|
||||
if (!singleton_variant(&classdb, "ClassDB")) { free(cache_path); return 0; }
|
||||
name_variant(&class_arg, "GDScript");
|
||||
const GDExtensionConstVariantPtr class_args[] = { &class_arg };
|
||||
int ok = invoke(&classdb, "instantiate", class_args, 1, &retained_script);
|
||||
script_live = 1;
|
||||
api_destroy(&class_arg);
|
||||
api_destroy(&classdb);
|
||||
if (!ok || api_type(&retained_script) != GDEXTENSION_VARIANT_TYPE_OBJECT) { free(cache_path); return 0; }
|
||||
string_variant(&source, (const char *)AGC_EMBEDDED_BRIDGE);
|
||||
const GDExtensionConstVariantPtr source_args[] = { &source };
|
||||
ok = invoke(&retained_script, "set_source_code", source_args, 1, &result);
|
||||
api_destroy(&result);
|
||||
api_destroy(&source);
|
||||
if (!ok) { free(cache_path); return 0; }
|
||||
ok = invoke(&retained_script, "reload", NULL, 0, &result);
|
||||
int64_t reload_error = -1;
|
||||
if (ok && api_type(&result) == GDEXTENSION_VARIANT_TYPE_INT) to_int(&reload_error, &result);
|
||||
api_destroy(&result);
|
||||
if (!ok || reload_error != 0) { free(cache_path); report_failure("bridge_compile", (int)reload_error); return 0; }
|
||||
ok = invoke(&retained_script, "new", NULL, 0, &retained_node);
|
||||
node_live = 1;
|
||||
if (!ok || api_type(&retained_node) != GDEXTENSION_VARIANT_TYPE_OBJECT) { free(cache_path); return 0; }
|
||||
Storage method, build, process_identity, cache;
|
||||
name_variant(&method, "bootstrap");
|
||||
string_variant(&build, AGC_BUILD_ID);
|
||||
string_variant(&process_identity, started_file_time);
|
||||
string_variant(&cache, cache_path);
|
||||
free(cache_path);
|
||||
const GDExtensionConstVariantPtr deferred[] = { &method, &build, &process_identity, &cache };
|
||||
ok = invoke(&retained_node, "call_deferred", deferred, 4, &result);
|
||||
api_destroy(&result);
|
||||
api_destroy(&method);
|
||||
api_destroy(&build);
|
||||
api_destroy(&process_identity);
|
||||
api_destroy(&cache);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void initialize_bridge(void *userdata, GDExtensionInitializationLevel level) {
|
||||
(void)userdata;
|
||||
if (level != GDEXTENSION_INITIALIZATION_EDITOR || started) return;
|
||||
started = 1;
|
||||
if (!schedule_bridge()) release_references();
|
||||
}
|
||||
|
||||
static void deinitialize_bridge(void *userdata, GDExtensionInitializationLevel level) {
|
||||
(void)userdata;
|
||||
if (level != GDEXTENSION_INITIALIZATION_EDITOR) return;
|
||||
if (node_live && api_type(&retained_node) == GDEXTENSION_VARIANT_TYPE_OBJECT) {
|
||||
Storage returned;
|
||||
invoke(&retained_node, "native_deinitialize", NULL, 0, &returned);
|
||||
api_destroy(&returned);
|
||||
}
|
||||
release_references();
|
||||
}
|
||||
|
||||
__declspec(dllexport) GDExtensionBool agc_godot_editor_init(
|
||||
GDExtensionInterfaceGetProcAddress get_proc_address,
|
||||
GDExtensionClassLibraryPtr library,
|
||||
GDExtensionInitialization *initialization) {
|
||||
(void)library;
|
||||
if (!get_proc_address || !initialization) return 0;
|
||||
#define LOAD(variable, type, symbol) do { \
|
||||
GDExtensionInterfaceFunctionPtr raw_function = get_proc_address(symbol); \
|
||||
_Static_assert(sizeof(type) == sizeof(raw_function), "Windows function pointer ABI mismatch"); \
|
||||
memcpy(&(variable), &raw_function, sizeof(variable)); \
|
||||
if (!variable) return 0; \
|
||||
} while (0)
|
||||
LOAD(api_warning, GDExtensionInterfacePrintWarning, "print_warning");
|
||||
LOAD(api_call, GDExtensionInterfaceVariantCall, "variant_call");
|
||||
LOAD(api_destroy, GDExtensionInterfaceVariantDestroy, "variant_destroy");
|
||||
LOAD(api_type, GDExtensionInterfaceVariantGetType, "variant_get_type");
|
||||
LOAD(api_singleton, GDExtensionInterfaceGlobalGetSingleton, "global_get_singleton");
|
||||
LOAD(api_name, GDExtensionInterfaceStringNameNewWithLatin1Chars, "string_name_new_with_latin1_chars");
|
||||
LOAD(api_string, GDExtensionInterfaceStringNewWithUtf8Chars, "string_new_with_utf8_chars");
|
||||
LOAD(api_utf8, GDExtensionInterfaceStringToUtf8Chars, "string_to_utf8_chars");
|
||||
GDExtensionInterfaceGetVariantFromTypeConstructor get_from;
|
||||
GDExtensionInterfaceGetVariantToTypeConstructor get_to;
|
||||
GDExtensionInterfaceVariantGetPtrDestructor get_destructor;
|
||||
LOAD(get_from, GDExtensionInterfaceGetVariantFromTypeConstructor, "get_variant_from_type_constructor");
|
||||
LOAD(get_to, GDExtensionInterfaceGetVariantToTypeConstructor, "get_variant_to_type_constructor");
|
||||
LOAD(get_destructor, GDExtensionInterfaceVariantGetPtrDestructor, "variant_get_ptr_destructor");
|
||||
#undef LOAD
|
||||
from_object = get_from(GDEXTENSION_VARIANT_TYPE_OBJECT);
|
||||
from_string = get_from(GDEXTENSION_VARIANT_TYPE_STRING);
|
||||
from_name = get_from(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
|
||||
to_int = get_to(GDEXTENSION_VARIANT_TYPE_INT);
|
||||
to_string = get_to(GDEXTENSION_VARIANT_TYPE_STRING);
|
||||
destroy_name = get_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
|
||||
destroy_string = get_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
|
||||
if (!from_object || !from_string || !from_name || !to_int || !to_string || !destroy_name || !destroy_string) return 0;
|
||||
initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_EDITOR;
|
||||
initialization->userdata = NULL;
|
||||
initialization->initialize = initialize_bridge;
|
||||
initialization->deinitialize = deinitialize_bridge;
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
#include <godot_cpp/classes/gd_script.hpp>
|
||||
#include <godot_cpp/classes/node.hpp>
|
||||
#include <godot_cpp/classes/project_settings.hpp>
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/core/object.hpp>
|
||||
#include <godot_cpp/godot.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "embedded_bridge.h"
|
||||
|
||||
namespace {
|
||||
using namespace godot;
|
||||
|
||||
// Godot 值的析构必须发生在官方绑定终止之前,不能依赖 DLL 静态析构顺序。
|
||||
struct BridgeState {
|
||||
Ref<GDScript> script;
|
||||
uint64_t node_id = 0;
|
||||
};
|
||||
BridgeState *bridge = nullptr;
|
||||
|
||||
bool plain_directory(const std::wstring &path) {
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & FILE_ATTRIBUTE_DIRECTORY) &&
|
||||
!(attributes & FILE_ATTRIBUTE_REPARSE_POINT);
|
||||
}
|
||||
|
||||
bool append_cache_directory(std::wstring &path, const wchar_t *part) {
|
||||
if (path.back() != L'\\')
|
||||
path += L'\\';
|
||||
path += part;
|
||||
if (!CreateDirectoryW(path.c_str(), nullptr) &&
|
||||
GetLastError() != ERROR_ALREADY_EXISTS)
|
||||
return false;
|
||||
return plain_directory(path);
|
||||
}
|
||||
|
||||
String session_cache_path() {
|
||||
const String project_root =
|
||||
ProjectSettings::get_singleton()->globalize_path("res://");
|
||||
const Char16String utf16 = project_root.utf16();
|
||||
static_assert(sizeof(wchar_t) == sizeof(char16_t),
|
||||
"Windows UTF-16 path required");
|
||||
std::wstring path(reinterpret_cast<const wchar_t *>(utf16.get_data()),
|
||||
utf16.length());
|
||||
if (path.size() < 3 || path.size() > 32000 || path[1] != L':')
|
||||
return {};
|
||||
for (wchar_t &character : path)
|
||||
if (character == L'/')
|
||||
character = L'\\';
|
||||
// 项目根及其所有祖先都须为普通本地目录,不经过链接或 junction。
|
||||
for (size_t index = 3; index <= path.size(); ++index) {
|
||||
if (index == path.size() || path[index] == L'\\') {
|
||||
if (!plain_directory(path.substr(0, index)))
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (!append_cache_directory(path, L".godot") ||
|
||||
!append_cache_directory(path, L"agc"))
|
||||
return {};
|
||||
path +=
|
||||
L"\\editor-bridge-" + std::to_wstring(GetCurrentProcessId()) + L".json";
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
if (attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)))
|
||||
return {};
|
||||
return String::utf16(reinterpret_cast<const char16_t *>(path.c_str()));
|
||||
}
|
||||
|
||||
bool schedule_bridge() {
|
||||
FILETIME creation, exit_time, kernel, user;
|
||||
if (!GetProcessTimes(GetCurrentProcess(), &creation, &exit_time, &kernel,
|
||||
&user))
|
||||
return false;
|
||||
ULARGE_INTEGER timestamp;
|
||||
timestamp.LowPart = creation.dwLowDateTime;
|
||||
timestamp.HighPart = creation.dwHighDateTime;
|
||||
const String started = String::num_uint64(timestamp.QuadPart);
|
||||
const String cache = session_cache_path();
|
||||
if (cache.is_empty())
|
||||
return false;
|
||||
|
||||
bridge->script.instantiate();
|
||||
bridge->script->set_source_code(
|
||||
String::utf8(reinterpret_cast<const char *>(AGC_EMBEDDED_BRIDGE)));
|
||||
if (bridge->script->reload() != OK)
|
||||
return false;
|
||||
const Variant instance = bridge->script->call("new");
|
||||
Node *node = Object::cast_to<Node>(static_cast<Object *>(instance));
|
||||
if (!node)
|
||||
return false;
|
||||
bridge->node_id = node->get_instance_id();
|
||||
node->call_deferred("bootstrap", AGC_BUILD_ID, started, cache);
|
||||
return true;
|
||||
}
|
||||
|
||||
void initialize_bridge(ModuleInitializationLevel level) {
|
||||
if (level != MODULE_INITIALIZATION_LEVEL_EDITOR || bridge)
|
||||
return;
|
||||
bridge = new BridgeState;
|
||||
if (!schedule_bridge()) {
|
||||
UtilityFunctions::push_warning(
|
||||
"AGC Godot editor bridge initialization failed");
|
||||
delete bridge;
|
||||
bridge = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void deinitialize_bridge(ModuleInitializationLevel level) {
|
||||
if (level != MODULE_INITIALIZATION_LEVEL_EDITOR)
|
||||
return;
|
||||
if (bridge) {
|
||||
// bootstrap 失败或编辑器退出时 Node 可能先被销毁,不能保留悬空指针。
|
||||
if (Object *node = ObjectDB::get_instance(bridge->node_id)) {
|
||||
const GDExtensionObjectPtr owner = node->_owner;
|
||||
node->call("native_deinitialize");
|
||||
// GDScript 的 queue_free 晚于 DLL 卸载;只移除 C++ 包装的回调,不销毁引擎
|
||||
// Node。
|
||||
internal::gdextension_interface_object_free_instance_binding(
|
||||
owner, internal::token);
|
||||
}
|
||||
// 脚本仍可能被当前 GDScript 调用栈引用。用 Variant 保活引擎对象,先释放 C++
|
||||
// Ref, 再解除 DLL 内的包装回调;保活值在本回调返回前销毁。
|
||||
const Variant script_lifetime = bridge->script;
|
||||
const GDExtensionObjectPtr script_owner =
|
||||
bridge->script.is_valid() ? bridge->script->_owner : nullptr;
|
||||
bridge->script.unref();
|
||||
if (script_owner)
|
||||
internal::gdextension_interface_object_free_instance_binding(
|
||||
script_owner, internal::token);
|
||||
delete bridge;
|
||||
bridge = nullptr;
|
||||
}
|
||||
// 晚加载扩展只收到 EDITOR 生命周期,官方单例包装清理原本位于 CORE 终止阶段。
|
||||
ClassDB::deinitialize(GDEXTENSION_INITIALIZATION_CORE);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" GDExtensionBool GDE_EXPORT
|
||||
agc_godot_editor_init(GDExtensionInterfaceGetProcAddress get_proc_address,
|
||||
GDExtensionClassLibraryPtr library,
|
||||
GDExtensionInitialization *initialization) {
|
||||
godot::GDExtensionBinding::InitObject init(get_proc_address, library,
|
||||
initialization);
|
||||
init.register_initializer(initialize_bridge);
|
||||
init.register_terminator(deinitialize_bridge);
|
||||
init.set_minimum_library_initialization_level(
|
||||
godot::MODULE_INITIALIZATION_LEVEL_EDITOR);
|
||||
return init.init();
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
spec = importlib.util.spec_from_file_location("dependencies", Path(__file__).parents[1] / "prepare_dependencies.py")
|
||||
dependencies = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(dependencies)
|
||||
|
||||
|
||||
class DependencyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.commit = "a" * 40
|
||||
self.name = f"godot-cpp-{self.commit}"
|
||||
self.archive = self.root / ".build/dependencies" / f"{self.commit}.zip"
|
||||
self.archive.parent.mkdir(parents=True)
|
||||
(self.root / "vendor").mkdir()
|
||||
(self.root / "vendor/LICENSE.txt").write_bytes(b"MIT\r\n")
|
||||
self.write_archive({"LICENSE.md": b"MIT\n", "src/core.cpp": b"verified"})
|
||||
|
||||
def write_archive(self, entries):
|
||||
with zipfile.ZipFile(self.archive, "w") as output:
|
||||
for name, content in entries.items():
|
||||
output.writestr(f"{self.name}/{name}", content)
|
||||
provenance = {"commit": self.commit,
|
||||
"archiveUrl": f"https://codeload.github.com/godotengine/godot-cpp/zip/{self.commit}",
|
||||
"archiveSha256": hashlib.sha256(self.archive.read_bytes()).hexdigest()}
|
||||
(self.root / "vendor/provenance.json").write_text(json.dumps(provenance), encoding="utf-8")
|
||||
|
||||
def test_verified_archive_is_reusable_offline(self):
|
||||
with patch("urllib.request.urlopen", side_effect=AssertionError("Unexpected network")):
|
||||
source = dependencies.prepare(self.root)
|
||||
self.assertEqual(source, dependencies.prepare(self.root))
|
||||
self.assertEqual((source / "src/core.cpp").read_bytes(), b"verified")
|
||||
|
||||
def test_corrupt_archive_is_rejected_and_preserved(self):
|
||||
self.archive.write_bytes(b"corrupt")
|
||||
with self.assertRaisesRegex(ValueError, "SHA256 mismatch"):
|
||||
dependencies.prepare(self.root)
|
||||
self.assertEqual(self.archive.read_bytes(), b"corrupt")
|
||||
|
||||
def test_modified_source_is_rejected_even_with_valid_archive(self):
|
||||
source = dependencies.prepare(self.root)
|
||||
(source / "src/core.cpp").write_bytes(b"modified")
|
||||
with self.assertRaisesRegex(ValueError, "Modified godot-cpp"):
|
||||
dependencies.prepare(self.root)
|
||||
self.assertEqual((source / "src/core.cpp").read_bytes(), b"modified")
|
||||
|
||||
def test_extra_source_is_rejected(self):
|
||||
source = dependencies.prepare(self.root)
|
||||
(source / "extra.cpp").write_text("unexpected", encoding="utf-8")
|
||||
with self.assertRaisesRegex(ValueError, "Unexpected dependency"):
|
||||
dependencies.prepare(self.root)
|
||||
|
||||
def test_archive_paths_cannot_escape_on_windows_or_posix(self):
|
||||
for name in ["../../escaped", "C:\\escaped", "..\\escaped"]:
|
||||
with self.subTest(name=name):
|
||||
self.write_archive({name: b"unexpected"})
|
||||
with self.assertRaisesRegex(ValueError, "Unsafe dependency"):
|
||||
dependencies.prepare(self.root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,5 +1,6 @@
|
||||
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
|
||||
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2017-present Godot Engine contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"project": "Godot Engine",
|
||||
"version": "4.7.2-stable",
|
||||
"commit": "ed1daf0bf001b61586d9930840f2f1394092c079",
|
||||
"project": "godot-cpp",
|
||||
"version": "godot-4.5-stable",
|
||||
"commit": "e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77",
|
||||
"license": "MIT",
|
||||
"licenseFile": "LICENSE.txt",
|
||||
"interfaceSource": "https://github.com/godotengine/godot/blob/ed1daf0bf001b61586d9930840f2f1394092c079/core/extension/gdextension_interface.json",
|
||||
"headerGenerator": "https://github.com/godotengine/godot/blob/ed1daf0bf001b61586d9930840f2f1394092c079/core/extension/make_interface_header.py",
|
||||
"generation": "Official unmodified generator using local file IO helpers; include guard and provenance comments added. No godot-cpp dependency."
|
||||
"archiveUrl": "https://codeload.github.com/godotengine/godot-cpp/zip/e83fd0904c13356ed1d4c3d09f8bb9132bdc6b77",
|
||||
"archiveSha256": "579af30c5f62c1084edb28216788188227d80a583f0114463699ac4edd22140f",
|
||||
"generation": "Unmodified official C++ bindings and interface, generated using the checked-in minimal build profile and statically linked into the editor DLL."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user