mirror of
https://github.com/Heretek-AI/godot-opencode.git
synced 2026-11-07 10:56:02 -10:00
fix(engine-47): make companion addon load and serve on Godot 4.7 (Refs #7)
This commit is contained in:
1 parent
c4cc12233a
commit
759985f57e
8 files changed
+107
-20
No files matched your search
@@ -37,7 +37,10 @@ var dispatcher = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(false)
|
||||
# NOTE: _ready runs AFTER the parent plugin's _enter_tree (which calls
|
||||
# startup()), so this must not force processing off — derive it from the
|
||||
# actual listen state instead.
|
||||
set_process(_server.is_listening())
|
||||
|
||||
|
||||
## Start listening. `capability_path` points at the 9500.json record.
|
||||
@@ -51,7 +54,7 @@ func startup(capability_path: String) -> int:
|
||||
if not _is_ws_token(_ws_capability):
|
||||
_ws_capability = ""
|
||||
return ERR_FILE_CORRUPT
|
||||
var err := _server.listen(BRIDGE_PORT, "127.0.0.1")
|
||||
var err: int = _server.listen(BRIDGE_PORT, "127.0.0.1")
|
||||
if err != OK:
|
||||
return err
|
||||
set_process(true)
|
||||
@@ -98,7 +101,7 @@ func _poll_peers() -> void:
|
||||
if not (peer is WebSocketPeer):
|
||||
continue
|
||||
peer.poll()
|
||||
var state := peer.get_ready_state()
|
||||
var state: int = peer.get_ready_state()
|
||||
if state == WebSocketPeer.STATE_CLOSED:
|
||||
_authenticated.erase(peer.get_instance_id())
|
||||
continue
|
||||
@@ -151,7 +154,9 @@ func _forward_command_frame(peer_id: int, raw: String) -> void:
|
||||
|
||||
## Route one dispatcher response to the peer that issued its request_id.
|
||||
## Returns false when the peer is gone (response dropped, never requeued).
|
||||
func send_response(response: Dictionary) -> bool: var peer := _peer_for_request(str(response.get("request_id", "")))
|
||||
## (Multiline body: Godot 4.7 rejects a same-line suite after the colon here.)
|
||||
func send_response(response: Dictionary) -> bool:
|
||||
var peer := _peer_for_request(str(response.get("request_id", "")))
|
||||
if peer == null:
|
||||
return false
|
||||
if peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
||||
|
||||
@@ -19,19 +19,20 @@ var _pending_queries: Dictionary = {}
|
||||
|
||||
|
||||
func _setup_session(session_id: int) -> void:
|
||||
var session := get_session(session_id)
|
||||
if session == null:
|
||||
return
|
||||
session.register_message_capture(
|
||||
CAPTURE_CHANNEL,
|
||||
func(message: StringName, data: Array) -> bool:
|
||||
return _on_capture(message, data)
|
||||
)
|
||||
_sessions.append(session_id)
|
||||
if not _sessions.has(session_id):
|
||||
_sessions.append(session_id)
|
||||
|
||||
|
||||
func _has_capture() -> void:
|
||||
_sessions.clear()
|
||||
## Engine capture entry point: the editor routes channel traffic to this
|
||||
## plugin when _has_capture reports the channel as handled.
|
||||
func _capture(message: String, data: Array, _session_id: int) -> bool:
|
||||
return _on_capture(message, data)
|
||||
|
||||
|
||||
## EditorDebuggerPlugin virtual (parent: `_has_capture(String) -> bool`):
|
||||
## report whether this plugin captures the given channel.
|
||||
func _has_capture(capture: String) -> bool:
|
||||
return capture == CAPTURE_CHANNEL
|
||||
|
||||
|
||||
## Engine capture callback. Returns true when the message was consumed.
|
||||
|
||||
@@ -62,7 +62,7 @@ func instantiate_scene(params: Dictionary) -> Dictionary:
|
||||
var node_name := str(params.get("name", ""))
|
||||
if not node_name.is_empty():
|
||||
instance.name = node_name
|
||||
var result := _undo.add_node_by_path(scene_root, str(params.get("parent_path", "/")), instance)
|
||||
var result: Dictionary = _undo.add_node_by_path(scene_root, str(params.get("parent_path", "/")), instance)
|
||||
if result.has("data"):
|
||||
result["data"]["scene_path"] = scene_path
|
||||
return result
|
||||
|
||||
@@ -24,7 +24,7 @@ func search_resources(params: Dictionary) -> Dictionary:
|
||||
var directory := queue.pop_back()
|
||||
seen += 1
|
||||
for entry in DirAccess.get_files_at(directory):
|
||||
var full := directory.path_join(entry)
|
||||
var full: String = directory.path_join(entry)
|
||||
if not full.match("*" + pattern + "*"):
|
||||
continue
|
||||
if not type_hint.is_empty() and not _type_matches(full, type_hint):
|
||||
|
||||
@@ -23,6 +23,12 @@ const BRIDGE_PORT := 9500
|
||||
var _connection: Node = null
|
||||
var _dispatcher: RefCounted = null
|
||||
var _debugger_plugin: EditorDebuggerPlugin = null
|
||||
## Retain every RefCounted handler for the plugin's lifetime. Callables bound
|
||||
## to a RefCounted method do NOT extend its lifetime: once the constructing
|
||||
## local goes out of scope the object is freed and every dispatch fails with
|
||||
## `null::<method> (Callable)` (proven live on Godot 4.7). Nodes (_connection)
|
||||
## are kept alive by the scene tree; RefCounteds need this explicit anchor.
|
||||
var _handler_refs: Array = []
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
@@ -34,6 +40,7 @@ func _enter_tree() -> void:
|
||||
var shaders := ShaderHandler.new()
|
||||
var themes := ThemeHandler.new(undo_manager)
|
||||
var viewport := ViewportHandler.new()
|
||||
_handler_refs = [undo_manager, nodes, signals, resources, shaders, themes, viewport]
|
||||
_register_node_commands(nodes)
|
||||
_register_signal_commands(signals)
|
||||
_register_resource_commands(resources)
|
||||
@@ -47,7 +54,7 @@ func _enter_tree() -> void:
|
||||
_debugger_plugin = DebuggerPlugin.new()
|
||||
_debugger_plugin.forward_target = _connection
|
||||
add_debugger_plugin(_debugger_plugin)
|
||||
var err := _connection.startup(_capability_record_path())
|
||||
var err: int = _connection.startup(_capability_record_path())
|
||||
if err != OK:
|
||||
push_warning("GodotOpenCode: bridge not listening (%s)" % error_string(err))
|
||||
|
||||
|
||||
@@ -66,8 +66,21 @@ func _undo_reinsert_by_path(scene_root: Node, parent_path: String, node: Node, i
|
||||
|
||||
## Rename the node at `node_path`. Undo resolves the node by its NEW path
|
||||
## (post-rename) so a rename-then-reorder sequence still finds it.
|
||||
## Node-name rule (verified against Godot 4.7): non-empty, and none of the
|
||||
## characters the engine silently rewrites on add_child (":", "/", ".",
|
||||
## "@", '"', "%"). Rejected names fail closed with INVALID_PARAMS instead
|
||||
## of being stored mangled.
|
||||
static func is_valid_node_name(value: String) -> bool:
|
||||
if value.is_empty():
|
||||
return false
|
||||
for ch in [":", "/", ".", "@", "\"", "%"]:
|
||||
if ch in value:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func rename_node_by_path(scene_root: Node, node_path: String, new_name: String) -> Dictionary:
|
||||
if new_name.is_empty() or not new_name.is_valid_node_name():
|
||||
if not is_valid_node_name(new_name):
|
||||
return {"error": {"code": "INVALID_PARAMS", "message": "Invalid node name: %s" % new_name}}
|
||||
var target := _resolve_by_path(scene_root, node_path)
|
||||
if target == null:
|
||||
|
||||
@@ -94,7 +94,11 @@ describe("ticket 2.6: out-of-band debugger and engine bridge", () => {
|
||||
expect(existsSync(PLUGIN)).toBe(true);
|
||||
const src = readFileSync(PLUGIN, "utf8");
|
||||
expect(src).toContain("extends EditorDebuggerPlugin");
|
||||
expect(src).toContain("register_message_capture(");
|
||||
// Live-verified on Godot 4.7: captures route via the _has_capture/_capture
|
||||
// virtuals. (register_message_capture does not exist on
|
||||
// EditorDebuggerSession — asserting it here pinned the bug.)
|
||||
expect(src).toContain("func _has_capture(capture: String) -> bool");
|
||||
expect(src).toContain("func _capture(");
|
||||
expect(src).toContain('CAPTURE_CHANNEL := "opencode"');
|
||||
expect(src).toContain("game_exception");
|
||||
expect(src).toContain("stack");
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Engine GDScript parse gate — regression pin for the v1.0.0 finding that the
|
||||
# companion addon shipped with parse errors no mock harness could catch
|
||||
# (same-line suite in connection.gd, String.is_valid_node_name, wrong
|
||||
# _has_capture override, uninferrable :=). Stages companion/ into a throwaway
|
||||
# Godot project and --check-only compiles every .gd file.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/check-gdscript.sh [--godot-bin "<bin>"]
|
||||
# GODOT_BIN env is honored when the flag is absent. The binary may be a
|
||||
# multi-word launcher, e.g.:
|
||||
# scripts/check-gdscript.sh --godot-bin "flatpak run --branch=stable --arch=x86_64 --command=godot org.godotengine.Godot"
|
||||
# No binary on PATH -> prints "unavailable" and exits 0 (CI-tolerated,
|
||||
# mirroring the verify/ unavailable legs). Any SCRIPT ERROR -> exit 1.
|
||||
set -u
|
||||
|
||||
BIN="${GODOT_BIN:-godot}"
|
||||
if [ "${1:-}" = "--godot-bin" ]; then
|
||||
BIN="${2:?missing binary after --godot-bin}"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
if ! command -v ${BIN%% *} >/dev/null 2>&1; then
|
||||
echo "check-gdscript: unavailable (no Godot binary) — skipped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# Stage under $HOME: Flatpak-sandboxed engines cannot see /tmp, so mktemp(1)'s
|
||||
# default glimpse outside the sandbox. A cache dir in $HOME is visible both
|
||||
# inside and outside the sandbox.
|
||||
STAGE_BASE="${XDG_CACHE_HOME:-$HOME/.cache}/godot-parse-gate"
|
||||
mkdir -p "$STAGE_BASE"
|
||||
STAGE="$(mktemp -d "$STAGE_BASE/stage.XXXXXX")"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
mkdir -p "$STAGE/addons"
|
||||
cp -r "$ROOT/companion/addons/godot_opencode" "$STAGE/addons/godot_opencode"
|
||||
cp "$ROOT/companion/validate_scene.gd" "$STAGE/validate_scene.gd"
|
||||
printf 'config_version=5\n\n[application]\nconfig/name="ParseGate"\n' > "$STAGE/project.godot"
|
||||
|
||||
count=0
|
||||
fail=0
|
||||
while IFS= read -r f; do
|
||||
count=$((count + 1))
|
||||
rel="res://${f#$STAGE/}"
|
||||
# shellcheck disable=SC2086
|
||||
if ! out=$($BIN --headless --path "$STAGE" --check-only --script "$rel" 2>&1); then
|
||||
echo "FAIL: $rel"
|
||||
printf '%s\n' "$out" | grep "SCRIPT ERROR" | head -n 3
|
||||
fail=1
|
||||
fi
|
||||
done < <(find "$STAGE" -name "*.gd")
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "check-gdscript: FAIL ($count files)"
|
||||
exit 1
|
||||
fi
|
||||
echo "check-gdscript: ok ($count files)"
|
||||
Reference in new issue
Block a user