Rewrite debugger for Godot 4 support + improved maintainability (#452)

* Significantly rework the debugger to add Godot 4 support.

* Simplify debugger internal message handling and shorten code paths, to enable easier maintenance in the future.

* Streamline debugger configs: almost all fields are now optional, and the debugger should work out-of-the-box in a wider set of situations.

* Add guardrails, error handling, and input prompts to help guide the user to correct usage/configuration.

* Add the following commands:
  *  godotTools.debugger.debugCurrentFile
  *  godotTools.debugger.debugPinnedFile
  *  godotTools.debugger.pinFile
  *  godotTools.debugger.unpinFile
  *  godotTools.debugger.openPinnedFile

---------

Co-authored-by: RedMser <redmser.jj2@gmail.com>
Co-authored-by: Zachary Gardner <30502195+ZachIsAGardner@users.noreply.github.com>
This commit is contained in:
Daelon Suzuka
2023-11-12 10:46:44 -05:00
committed by GitHub
parent 55617fdd39
commit a4c1181894
48 changed files with 6805 additions and 4189 deletions

View File

@@ -1,7 +0,0 @@
import { Mediator } from "../mediator";
export abstract class Command {
public param_count: number = -1;
public abstract trigger(parameters: any[]): void;
}

View File

@@ -1,170 +0,0 @@
import { Command } from "./command";
import { CommandDebugEnter } from "./commands/command_debug_enter";
import { CommandOutput } from "./commands/command_output";
import { CommandStackDump } from "./commands/command_stack_dump";
import { CommandStackFrameVars } from "./commands/command_stack_frame_vars";
import { CommandNull } from "./commands/command_null";
import { CommandMessageSceneTree } from "./commands/command_message_scene_tree";
import { CommandMessageInspectObject } from "./commands/command_message_inspect_object";
import { CommandDebugExit } from "./commands/command_debug_exit";
import { VariantEncoder } from "../variables/variant_encoder";
export class CommandParser {
private commands: Map<string, () => Command> = new Map([
[
"output",
function () {
return new CommandOutput();
},
],
[
"message:scene_tree",
function () {
return new CommandMessageSceneTree();
},
],
[
"message:inspect_object",
function () {
return new CommandMessageInspectObject();
},
],
[
"stack_dump",
function () {
return new CommandStackDump();
},
],
[
"stack_frame_vars",
function () {
return new CommandStackFrameVars();
},
],
[
"debug_enter",
function () {
return new CommandDebugEnter();
},
],
[
"debug_exit",
function () {
return new CommandDebugExit();
},
],
]);
private current_command?: Command;
private encoder = new VariantEncoder();
private parameters: any[] = [];
public has_command() {
return this.current_command;
}
public make_break_command(): Buffer {
return this.build_buffered_command("break");
}
public make_continue_command(): Buffer {
return this.build_buffered_command("continue");
}
public make_inspect_object_command(object_id: bigint): Buffer {
return this.build_buffered_command("inspect_object", [object_id]);
}
public make_next_command(): Buffer {
return this.build_buffered_command("next");
}
public make_remove_breakpoint_command(path_to: string, line: number): Buffer {
return this.build_buffered_command("breakpoint", [path_to, line, false]);
}
public make_request_scene_tree_command(): Buffer {
return this.build_buffered_command("request_scene_tree");
}
public make_send_breakpoint_command(path_to: string, line: number): Buffer {
return this.build_buffered_command("breakpoint", [path_to, line, true]);
}
public make_set_object_value_command(
object_id: bigint,
label: string,
new_parsed_value: any
): Buffer {
return this.build_buffered_command("set_object_property", [
object_id,
label,
new_parsed_value,
]);
}
public make_stack_dump_command(): Buffer {
return this.build_buffered_command("get_stack_dump");
}
public make_stack_frame_vars_command(frame_id: number): Buffer {
return this.build_buffered_command("get_stack_frame_vars", [frame_id]);
}
public make_step_command() {
return this.build_buffered_command("step");
}
public parse_message(dataset: any[]) {
while (dataset && dataset.length > 0) {
if (this.current_command) {
this.parameters.push(dataset.shift());
if (this.current_command.param_count !== -1) {
if (this.current_command.param_count === this.parameters.length) {
try {
this.current_command.trigger(this.parameters);
} catch (e) {
// FIXME: Catch exception during trigger command: TypeError: class_name.replace is not a function
// class_name is the key of Mediator.inspect_callbacks
console.error("Catch exception during trigger command: " + e);
} finally {
this.current_command = undefined;
this.parameters = [];
}
} else if(this.current_command.param_count < this.parameters.length) {
// we debugged that an exception occures during this.current_command.trigger(this.parameters)
// because we do not understand the root cause of the exception, we set the current command to undefined
// to avoid a infinite loop of parse_message(...)
this.current_command = undefined;
this.parameters = [];
console.log("Exception not catched. Reset current_command to avoid infinite loop.");
}
} else {
this.current_command.param_count = this.parameters.shift();
if (this.current_command.param_count === 0) {
this.current_command.trigger([]);
this.current_command = undefined;
}
}
} else {
let data = dataset.shift();
if (data && this.commands.has(data)) {
this.current_command = this.commands.get(data)();
} else {
this.current_command = new CommandNull();
}
}
}
}
private build_buffered_command(command: string, parameters?: any[]) {
let command_array: any[] = [command];
if (parameters) {
parameters.forEach((param) => {
command_array.push(param);
});
}
let buffer = this.encoder.encode_variant(command_array);
return buffer;
}
}

View File

@@ -1,9 +0,0 @@
import { Command } from "../command";
import { Mediator } from "../../mediator";
export class CommandDebugEnter extends Command {
public trigger(parameters: any[]) {
let reason: string = parameters[1];
Mediator.notify("debug_enter", [reason]);
}
}

View File

@@ -1,8 +0,0 @@
import { Command } from "../command";
import { Mediator } from "../../mediator";
export class CommandDebugExit extends Command {
public trigger(parameters: any[]) {
Mediator.notify("debug_exit");
}
}

View File

@@ -1,18 +0,0 @@
import { Command } from "../command";
import { RawObject } from "../../variables/variants";
import { Mediator } from "../../mediator";
export class CommandMessageInspectObject extends Command {
public trigger(parameters: any[]) {
let id = BigInt(parameters[0]);
let class_name: string = parameters[1];
let properties: any[] = parameters[2];
let raw_object = new RawObject(class_name);
properties.forEach((prop) => {
raw_object.set(prop[0], prop[5]);
});
Mediator.notify("inspected_object", [id, raw_object]);
}
}

View File

@@ -1,25 +0,0 @@
import { Command } from "../command";
import { Mediator } from "../../mediator";
import { SceneNode } from "../../scene_tree/scene_tree_provider";
export class CommandMessageSceneTree extends Command {
public trigger(parameters: any[]) {
let scene = this.parse_next(parameters, { offset: 0 });
Mediator.notify("scene_tree", [scene]);
}
private parse_next(params: any[], ofs: { offset: number }): SceneNode {
let child_count: number = params[ofs.offset++];
let name: string = params[ofs.offset++];
let class_name: string = params[ofs.offset++];
let id: number = params[ofs.offset++];
let children: SceneNode[] = [];
for (let i = 0; i < child_count; ++i) {
children.push(this.parse_next(params, ofs));
}
return new SceneNode(name, class_name, id, children);
}
}

View File

@@ -1,5 +0,0 @@
import { Command } from "../command";
export class CommandNull extends Command {
public trigger(parameters: any[]) {}
}

View File

@@ -1,9 +0,0 @@
import { Command } from "../command";
import { Mediator } from "../../mediator";
export class CommandOutput extends Command {
public trigger(parameters: any[]) {
let lines: string[] = parameters;
Mediator.notify("output", lines);
}
}

View File

@@ -1,17 +0,0 @@
import { Command } from "../command";
import { Mediator } from "../../mediator";
import { GodotStackFrame } from "../../debug_runtime";
export class CommandStackDump extends Command {
public trigger(parameters: any[]) {
let frames: GodotStackFrame[] = parameters.map((sf, i) => {
return {
id: i,
file: sf.get("file"),
function: sf.get("function"),
line: sf.get("line"),
};
});
Mediator.notify("stack_dump", frames);
}
}

View File

@@ -1,31 +0,0 @@
import { Command } from "../command";
import { Mediator } from "../../mediator";
export class CommandStackFrameVars extends Command {
public trigger(parameters: any[]) {
let globals: any[] = [];
let locals: any[] = [];
let members: any[] = [];
let local_count = parameters[0] * 2;
let member_count = parameters[1 + local_count] * 2;
let global_count = parameters[2 + local_count + member_count] * 2;
if (local_count > 0) {
let offset = 1;
locals = parameters.slice(offset, offset + local_count);
}
if (member_count > 0) {
let offset = 2 + local_count;
members = parameters.slice(offset, offset + member_count);
}
if (global_count > 0) {
let offset = 3 + local_count + member_count;
globals = parameters.slice(offset, offset + global_count);
}
Mediator.notify("stack_frame_vars", [locals, members, globals]);
}
}

View File

@@ -1,3 +0,0 @@
import { GodotDebugSession } from "./debug_session";
GodotDebugSession.run(GodotDebugSession);

View File

@@ -1,6 +1,8 @@
import { Mediator } from "./mediator";
import { SceneTreeProvider } from "./scene_tree/scene_tree_provider";
const path = require("path");
import { SceneTreeProvider } from "./scene_tree_provider";
import path = require("path");
import { createLogger } from "../logger";
const log = createLogger("debugger.runtime");
export interface GodotBreakpoint {
file: string;
@@ -15,11 +17,64 @@ export interface GodotStackFrame {
line: number;
}
export class GodotStackVars {
public remaining = 0;
constructor(
public locals: GodotVariable[] = [],
public members: GodotVariable[] = [],
public globals: GodotVariable[] = [],
) { }
public reset(count: number = 0) {
this.locals = [];
this.members = [];
this.globals = [];
this.remaining = count;
}
public forEach(callbackfn: (value: GodotVariable, index: number, array: GodotVariable[]) => void, thisArg?: any) {
this.locals.forEach(callbackfn);
this.members.forEach(callbackfn);
this.globals.forEach(callbackfn);
}
}
export interface GodotVariable {
name: string;
scope_path?: string;
sub_values?: GodotVariable[];
value: any;
type?: bigint;
id?: bigint;
}
export interface GDObject {
stringify_value(): string;
sub_values(): GodotVariable[];
type_name(): string;
}
export class RawObject extends Map<any, any> {
constructor(public class_name: string) {
super();
}
}
export class ObjectId implements GDObject {
constructor(public id: bigint) { }
public stringify_value(): string {
return `<${this.id}>`;
}
public sub_values(): GodotVariable[] {
return [{ name: "id", value: this.id }];
}
public type_name(): string {
return "Object";
}
}
export class GodotDebugData {
@@ -28,47 +83,18 @@ export class GodotDebugData {
public last_frame: GodotStackFrame;
public last_frames: GodotStackFrame[] = [];
public project_path: string;
public projectPath: string;
public scene_tree?: SceneTreeProvider;
public stack_count: number = 0;
public stack_files: string[] = [];
public session;
public constructor() {}
public get_all_breakpoints(): GodotBreakpoint[] {
let output: GodotBreakpoint[] = [];
Array.from(this.breakpoints.values()).forEach((bp_array) => {
output.push(...bp_array);
});
return output;
}
public get_breakpoints(path: string) {
return this.breakpoints.get(path) || [];
}
public remove_breakpoint(path_to: string, line: number) {
let bps = this.breakpoints.get(path_to);
if (bps) {
let index = bps.findIndex((bp) => {
return bp.line === line;
});
if (index !== -1) {
let bp = bps[index];
bps.splice(index, 1);
this.breakpoints.set(path_to, bps);
let file = `res://${path.relative(this.project_path, bp.file)}`;
Mediator.notify("remove_breakpoint", [
file.replace(/\\/g, "/"),
bp.line,
]);
}
}
public constructor(session) {
this.session = session;
}
public set_breakpoint(path_to: string, line: number) {
let bp = {
const bp = {
file: path_to.replace(/\\/g, "/"),
line: line,
id: this.breakpoint_id++,
@@ -82,9 +108,65 @@ export class GodotDebugData {
bps.push(bp);
if (this.project_path) {
let out_file = `res://${path.relative(this.project_path, bp.file)}`;
Mediator.notify("set_breakpoint", [out_file.replace(/\\/g, "/"), line]);
if (this.projectPath) {
const out_file = `res://${path.relative(this.projectPath, bp.file)}`;
this.session?.controller.set_breakpoint(out_file.replace(/\\/g, "/"), line);
}
}
public remove_breakpoint(pathTo: string, line: number) {
const bps = this.breakpoints.get(pathTo);
if (bps) {
const index = bps.findIndex((bp) => {
return bp.line === line;
});
if (index !== -1) {
const bp = bps[index];
bps.splice(index, 1);
this.breakpoints.set(pathTo, bps);
const file = `res://${path.relative(this.projectPath, bp.file)}`;
this.session?.controller.remove_breakpoint(
file.replace(/\\/g, "/"),
bp.line,
);
}
}
}
public get_all_breakpoints(): GodotBreakpoint[] {
const output: GodotBreakpoint[] = [];
Array.from(this.breakpoints.values()).forEach((bp_array) => {
output.push(...bp_array);
});
return output;
}
public get_breakpoints(path: string) {
return this.breakpoints.get(path) || [];
}
public get_breakpoint_string() {
const breakpoints = this.get_all_breakpoints();
let output = "";
if (breakpoints.length > 0) {
output += " --breakpoints \"";
breakpoints.forEach((bp, i) => {
output += `${this.get_breakpoint_path(bp.file)}:${bp.line}`;
if (i < breakpoints.length - 1) {
output += ",";
}
});
output += "\"";
}
return output;
}
public get_breakpoint_path(file: string) {
const relativePath = path.relative(this.projectPath, file).replace(/\\/g, "/");
if (relativePath.length !== 0) {
return `res://${relativePath}`;
}
return undefined;
}
}

318
src/debugger/debugger.ts Normal file
View File

@@ -0,0 +1,318 @@
import * as fs from "fs";
import {
debug,
window,
workspace,
ExtensionContext,
DebugConfigurationProvider,
WorkspaceFolder,
DebugAdapterInlineImplementation,
DebugAdapterDescriptorFactory,
DebugConfiguration,
DebugAdapterDescriptor,
DebugSession,
CancellationToken,
ProviderResult,
Uri
} from "vscode";
import { DebugProtocol } from "@vscode/debugprotocol";
import { GodotDebugSession as Godot3DebugSession } from "./godot3/debug_session";
import { GodotDebugSession as Godot4DebugSession } from "./godot4/debug_session";
import { register_command, projectVersion, set_context } from "../utils";
import { SceneTreeProvider, SceneNode } from "./scene_tree_provider";
import { InspectorProvider, RemoteProperty } from "./inspector_provider";
import { createLogger } from "../logger";
const log = createLogger("debugger", { output: "Godot Debugger" });
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
address: string;
port: number;
project: string;
scene: string;
editor_path: string;
additional_options: string;
}
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
address: string;
port: number;
project: string;
scene: string;
additional_options: string;
}
export let pinnedScene: Uri;
export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfigurationProvider {
public session?: Godot3DebugSession | Godot4DebugSession;
public inspectorProvider = new InspectorProvider();
public sceneTreeProvider = new SceneTreeProvider();
constructor(private context: ExtensionContext) {
log.info("Initializing Godot Debugger");
context.subscriptions.push(
debug.registerDebugConfigurationProvider("godot", this),
debug.registerDebugAdapterDescriptorFactory("godot", this),
window.registerTreeDataProvider("inspectNode", this.inspectorProvider),
window.registerTreeDataProvider("activeSceneTree", this.sceneTreeProvider),
register_command("debugger.inspectNode", this.inspect_node.bind(this)),
register_command("debugger.refreshSceneTree", this.refresh_scene_tree.bind(this)),
register_command("debugger.refreshInspector", this.refresh_inspector.bind(this)),
register_command("debugger.editValue", this.edit_value.bind(this)),
register_command("debugger.debugCurrentFile", this.debug_current_file.bind(this)),
register_command("debugger.debugPinnedFile", this.debug_pinned_file.bind(this)),
register_command("debugger.pinFile", this.pin_file.bind(this)),
register_command("debugger.unpinFile", this.unpin_file.bind(this)),
register_command("debugger.openPinnedFile", this.open_pinned_file.bind(this)),
);
}
public createDebugAdapterDescriptor(session: DebugSession): ProviderResult<DebugAdapterDescriptor> {
log.info("Creating debug session");
log.info(`Project version identified as ${projectVersion}`);
if (projectVersion.startsWith("4")) {
this.session = new Godot4DebugSession();
} else {
this.session = new Godot3DebugSession();
}
this.context.subscriptions.push(this.session);
this.session.sceneTree = this.sceneTreeProvider;
return new DebugAdapterInlineImplementation(this.session);
}
public resolveDebugConfiguration(
folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
token?: CancellationToken
): ProviderResult<DebugConfiguration> {
// request is actually a required field according to vscode
// however, setting it here lets us catch a possible misconfiguration
if (!config.request) {
config.request = "launch";
}
if (config.request === "launch") {
if (!config.address) {
config.address = "127.0.0.1";
}
if (!config.port) {
config.port = -1;
}
if (!config.project) {
config.project = "${workspaceFolder}";
}
}
return config;
}
public debug_current_file() {
log.info("Attempting to debug current file");
const configs: DebugConfiguration[] = workspace.getConfiguration("launch", window.activeTextEditor.document.uri).get("configurations");
const launches = configs.filter((c) => c.request === "launch");
const currents = configs.filter((c) => c.scene === "current");
let path = window.activeTextEditor.document.fileName;
if (path.endsWith(".gd")) {
const scenePath = path.replace(".gd", ".tscn");
if (!fs.existsSync(scenePath)) {
log.warn(`Can't find associated scene for '${path}', aborting debug`);
window.showWarningMessage(`Can't find associated scene file for '${path}'`);
return;
}
path = scenePath;
}
const default_config = {
name: `Debug ${path} : 'File'}`,
type: "godot",
request: "launch",
scene: "current",
};
const config = currents[0] ?? launches[0] ?? configs[0] ?? default_config;
config.scene = path;
log.info(`Starting debug session for '${path}'`);
debug.startDebugging(workspace.workspaceFolders[0], config);
}
public debug_pinned_file() {
log.info("Attempting to debug pinned scene");
const configs: DebugConfiguration[] = workspace.getConfiguration("launch", pinnedScene).get("configurations");
const launches = configs.filter((c) => c.request === "launch");
const currents = configs.filter((c) => c.scene === "pinned");
if (!pinnedScene) {
log.warn("No pinned scene found, aborting debug");
window.showWarningMessage("No pinned scene found");
return;
}
let path = pinnedScene.fsPath;
if (path.endsWith(".gd")) {
const scenePath = path.replace(".gd", ".tscn");
if (!fs.existsSync(scenePath)) {
log.warn(`Can't find associated scene for '${path}', aborting debug`);
window.showWarningMessage(`Can't find associated scene file for '${path}'`);
return;
}
path = scenePath;
}
const default_config = {
name: `Debug ${path} : 'File'}`,
type: "godot",
request: "launch",
scene: "pinned",
};
const config = currents[0] ?? launches[0] ?? configs[0] ?? default_config;
config.scene = path;
log.info(`Starting debug session for '${path}'`);
debug.startDebugging(workspace.workspaceFolders[0], config);
}
public pin_file(uri: Uri) {
if (uri === undefined) {
uri = window.activeTextEditor.document.uri;
}
log.info(`Pinning debug target file: '${uri.fsPath}'`);
set_context("pinnedScene", [uri.fsPath]);
pinnedScene = uri;
}
public unpin_file(uri: Uri) {
log.info(`Unpinning debug target file: '${pinnedScene}'`);
set_context("pinnedScene", []);
pinnedScene = undefined;
}
public open_pinned_file() {
log.info(`Opening pinned debug target file: '${pinnedScene}'`);
if (pinnedScene){
window.showTextDocument(pinnedScene);
}
}
public inspect_node(element: SceneNode | RemoteProperty) {
this.session?.controller.request_inspect_object(BigInt(element.object_id));
this.session?.inspect_callbacks.set(
BigInt(element.object_id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
element.label,
class_name,
element.object_id,
variable
);
},
);
}
public refresh_scene_tree() {
this.session?.controller.request_scene_tree();
}
public refresh_inspector() {
if (this.inspectorProvider.has_tree()) {
const name = this.inspectorProvider.get_top_name();
const id = this.inspectorProvider.get_top_id();
this.session?.controller.request_inspect_object(BigInt(id));
this.session?.inspect_callbacks.set(
BigInt(id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
name,
class_name,
id,
variable
);
},
);
}
}
public edit_value(property: RemoteProperty) {
const previous_value = property.value;
const type = typeof previous_value;
const is_float = type === "number" && !Number.isInteger(previous_value);
window
.showInputBox({ value: `${property.description}` })
.then((value) => {
let new_parsed_value: any;
switch (type) {
case "string":
new_parsed_value = value;
break;
case "number":
if (is_float) {
new_parsed_value = parseFloat(value);
if (isNaN(new_parsed_value)) {
return;
}
} else {
new_parsed_value = parseInt(value);
if (isNaN(new_parsed_value)) {
return;
}
}
break;
case "boolean":
if (
value.toLowerCase() === "true" ||
value.toLowerCase() === "false"
) {
new_parsed_value = value.toLowerCase() === "true";
} else if (value === "0" || value === "1") {
new_parsed_value = value === "1";
} else {
return;
}
}
if (property.changes_parent) {
const parents = [property.parent];
let idx = 0;
while (parents[idx].changes_parent) {
parents.push(parents[idx++].parent);
}
const changed_value = this.inspectorProvider.get_changed_value(
parents,
property,
new_parsed_value
);
this.session?.controller.set_object_property(
BigInt(property.object_id),
parents[idx].label,
changed_value,
);
} else {
this.session?.controller.set_object_property(
BigInt(property.object_id),
property.label,
new_parsed_value,
);
}
const name = this.inspectorProvider.get_top_name();
const id = this.inspectorProvider.get_top_id();
this.session?.controller.request_inspect_object(BigInt(id));
this.session?.inspect_callbacks.set(
BigInt(id),
(class_name, variable) => {
this.inspectorProvider.fill_tree(
name,
class_name,
id,
variable
);
},
);
});
}
}

View File

@@ -1,223 +0,0 @@
import {
ExtensionContext,
debug,
DebugConfigurationProvider,
WorkspaceFolder,
DebugAdapterInlineImplementation,
DebugAdapterDescriptorFactory,
DebugConfiguration,
DebugAdapterDescriptor,
DebugSession,
CancellationToken,
ProviderResult,
window,
commands,
} from "vscode";
import { GodotDebugSession } from "./debug_session";
import fs = require("fs");
import { SceneTreeProvider, SceneNode } from "./scene_tree/scene_tree_provider";
import {
RemoteProperty,
InspectorProvider,
} from "./scene_tree/inspector_provider";
import { Mediator } from "./mediator";
export function register_debugger(context: ExtensionContext) {
let provider = new GodotConfigurationProvider();
context.subscriptions.push(
debug.registerDebugConfigurationProvider("godot", provider)
);
let inspector_provider = new InspectorProvider();
window.registerTreeDataProvider("inspect-node", inspector_provider);
let scene_tree_provider = new SceneTreeProvider();
window.registerTreeDataProvider("active-scene-tree", scene_tree_provider);
let factory = new GodotDebugAdapterFactory(scene_tree_provider);
context.subscriptions.push(
debug.registerDebugAdapterDescriptorFactory("godot", factory)
);
commands.registerCommand(
"godotTools.debugger.inspectNode",
(element: SceneNode | RemoteProperty) => {
if (element instanceof SceneNode) {
Mediator.notify("inspect_object", [
element.object_id,
(class_name, variable) => {
inspector_provider.fill_tree(
element.label,
class_name,
element.object_id,
variable
);
},
]);
} else if (element instanceof RemoteProperty) {
Mediator.notify("inspect_object", [
element.object_id,
(class_name, properties) => {
inspector_provider.fill_tree(
element.label,
class_name,
element.object_id,
properties
);
},
]);
}
}
);
commands.registerCommand("godotTools.debugger.refreshSceneTree", () => {
Mediator.notify("request_scene_tree", []);
});
commands.registerCommand("godotTools.debugger.refreshInspector", () => {
if (inspector_provider.has_tree()) {
let name = inspector_provider.get_top_name();
let id = inspector_provider.get_top_id();
Mediator.notify("inspect_object", [
id,
(class_name, properties) => {
inspector_provider.fill_tree(name, class_name, id, properties);
},
]);
}
});
commands.registerCommand(
"godotTools.debugger.editValue",
(property: RemoteProperty) => {
let previous_value = property.value;
let type = typeof previous_value;
let is_float = type === "number" && !Number.isInteger(previous_value);
window
.showInputBox({ value: `${property.description}` })
.then((value) => {
let new_parsed_value: any;
switch (type) {
case "string":
new_parsed_value = value;
break;
case "number":
if (is_float) {
new_parsed_value = parseFloat(value);
if (isNaN(new_parsed_value)) {
return;
}
} else {
new_parsed_value = parseInt(value);
if (isNaN(new_parsed_value)) {
return;
}
}
break;
case "boolean":
if (
value.toLowerCase() === "true" ||
value.toLowerCase() === "false"
) {
new_parsed_value = value.toLowerCase() === "true";
} else if (value === "0" || value === "1") {
new_parsed_value = value === "1";
} else {
return;
}
}
if (property.changes_parent) {
let parents = [property.parent];
let idx = 0;
while (parents[idx].changes_parent) {
parents.push(parents[idx++].parent);
}
let changed_value = inspector_provider.get_changed_value(
parents,
property,
new_parsed_value
);
Mediator.notify("changed_value", [
property.object_id,
parents[idx].label,
changed_value,
]);
} else {
Mediator.notify("changed_value", [
property.object_id,
property.label,
new_parsed_value,
]);
}
Mediator.notify("inspect_object", [
inspector_provider.get_top_id(),
(class_name, properties) => {
inspector_provider.fill_tree(
inspector_provider.get_top_name(),
class_name,
inspector_provider.get_top_id(),
properties
);
},
]);
});
}
);
context.subscriptions.push(factory);
}
class GodotConfigurationProvider implements DebugConfigurationProvider {
public resolveDebugConfiguration(
folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
token?: CancellationToken
): ProviderResult<DebugConfiguration> {
if (!config.type && !config.request && !config.name) {
const editor = window.activeTextEditor;
if (editor && fs.existsSync(`${folder.uri.fsPath}/project.godot`)) {
config.type = "godot";
config.name = "Debug Godot";
config.request = "launch";
config.project = "${workspaceFolder}";
config.port = 6007;
config.address = "127.0.0.1";
config.launch_game_instance = true;
config.launch_scene = false;
config.additional_options = "";
}
}
if (!config.project) {
return window
.showInformationMessage(
"Cannot find a project.godot in active workspace."
)
.then(() => {
return undefined;
});
}
return config;
}
}
class GodotDebugAdapterFactory implements DebugAdapterDescriptorFactory {
public session: GodotDebugSession | undefined;
constructor(private scene_tree_provider: SceneTreeProvider) {}
public createDebugAdapterDescriptor(
session: DebugSession
): ProviderResult<DebugAdapterDescriptor> {
this.session = new GodotDebugSession();
this.session.set_scene_tree(this.scene_tree_provider);
return new DebugAdapterInlineImplementation(this.session);
}
public dispose() {
this.session.dispose();
this.session = undefined;
}
}

View File

@@ -1,84 +1,111 @@
import * as fs from "fs";
import {
Breakpoint, InitializedEvent, LoggingDebugSession, Source, Thread
} from "vscode-debugadapter";
import { DebugProtocol } from "vscode-debugprotocol";
import { get_configuration } from "../utils";
import { GodotDebugData, GodotVariable } from "./debug_runtime";
import { Mediator } from "./mediator";
import { SceneTreeProvider } from "./scene_tree/scene_tree_provider";
LoggingDebugSession,
InitializedEvent,
Thread,
Source,
Breakpoint,
StoppedEvent,
TerminatedEvent,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { debug } from "vscode";
import { Subject } from "await-notify";
import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime";
import { LaunchRequestArguments, AttachRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ObjectId } from "./variables/variants";
import { parse_variable, is_variable_built_in_type } from "./helpers";
import { ServerController } from "./server_controller";
import { ObjectId, RawObject } from "./variables/variants";
const { Subject } = require("await-notify");
import fs = require("fs");
import { createLogger } from "../../logger";
interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
address: string;
launch_game_instance: boolean;
launch_scene: boolean;
port: number;
project: string;
scene_file: string;
additional_options: string;
}
const log = createLogger("debugger.session", { output: "Godot Debugger" });
export class GodotDebugSession extends LoggingDebugSession {
private all_scopes: GodotVariable[];
private controller?: ServerController;
private debug_data = new GodotDebugData();
public controller = new ServerController(this);
public debug_data = new GodotDebugData(this);
public sceneTree: SceneTreeProvider;
private exception = false;
private got_scope = new Subject();
private got_scope: Subject = new Subject();
private ongoing_inspections: bigint[] = [];
private previous_inspections: bigint[] = [];
private configuration_done = new Subject();
private configuration_done: Subject = new Subject();
private mode: "launch" | "attach" | "" = "";
public inspect_callbacks: Map<
bigint,
(class_name: string, variable: GodotVariable) => void
> = new Map();
public constructor() {
super();
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);
Mediator.set_session(this);
this.controller = new ServerController();
Mediator.set_controller(this.controller);
Mediator.set_debug_data(this.debug_data);
}
public dispose() {}
public set_exception(exception: boolean) {
this.exception = true;
public dispose() {
this.controller.stop();
}
public set_inspection(id: bigint, replacement: GodotVariable) {
let variables = this.all_scopes.filter(
(va) => va && va.value instanceof ObjectId && va.value.id === id
);
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments
) {
response.body = response.body || {};
variables.forEach((va) => {
let index = this.all_scopes.findIndex((va_id) => va_id === va);
let old = this.all_scopes.splice(index, 1);
replacement.name = old[0].name;
replacement.scope_path = old[0].scope_path;
this.append_variable(replacement, index);
});
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsTerminateRequest = true;
response.body.supportsEvaluateForHovers = false;
response.body.supportsStepBack = false;
response.body.supportsGotoTargetsRequest = false;
response.body.supportsCancelRequest = false;
response.body.supportsCompletionsRequest = false;
response.body.supportsFunctionBreakpoints = false;
response.body.supportsDataBreakpoints = false;
response.body.supportsBreakpointLocationsRequest = false;
response.body.supportsConditionalBreakpoints = false;
response.body.supportsHitConditionalBreakpoints = false;
response.body.supportsLogPoints = false;
response.body.supportsModulesRequest = false;
response.body.supportsReadMemoryRequest = false;
response.body.supportsRestartFrame = false;
response.body.supportsRestartRequest = false;
response.body.supportsSetExpression = false;
response.body.supportsStepInTargetsRequest = false;
response.body.supportsTerminateThreadsRequest = false;
this.ongoing_inspections.splice(
this.ongoing_inspections.findIndex((va_id) => va_id === id),
1
);
this.previous_inspections.push(id);
this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
this.sendResponse(response);
this.sendEvent(new InitializedEvent());
}
public set_scene_tree(scene_tree_provider: SceneTreeProvider) {
this.debug_data.scene_tree = scene_tree_provider;
protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: LaunchRequestArguments
) {
await this.configuration_done.wait(1000);
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
}
protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: AttachRequestArguments
) {
await this.configuration_done.wait(1000);
this.mode = "attach";
this.exception = false;
await this.controller.attach(args);
this.sendResponse(response);
}
public configurationDoneRequest(
@@ -89,80 +116,35 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
public set_scopes(
locals: GodotVariable[],
members: GodotVariable[],
globals: GodotVariable[]
) {
this.all_scopes = [
undefined,
{ name: "local", value: undefined, sub_values: locals, scope_path: "@" },
{
name: "member",
value: undefined,
sub_values: members,
scope_path: "@",
},
{
name: "global",
value: undefined,
sub_values: globals,
scope_path: "@",
},
];
locals.forEach((va) => {
va.scope_path = `@.local`;
this.append_variable(va);
});
members.forEach((va) => {
va.scope_path = `@.member`;
this.append_variable(va);
});
globals.forEach((va) => {
va.scope_path = `@.global`;
this.append_variable(va);
});
this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
protected continueRequest(
response: DebugProtocol.ContinueResponse,
args: DebugProtocol.ContinueArguments
) {
if (!this.exception) {
response.body = { allThreadsContinued: true };
Mediator.notify("continue");
this.controller.continue();
this.sendResponse(response);
}
}
protected evaluateRequest(
protected async evaluateRequest(
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments
) {
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
if (this.all_scopes) {
let expression = args.expression;
let matches = expression.match(/^[_a-zA-Z0-9]+?$/);
if (matches) {
let result_idx = this.all_scopes.findIndex(
(va) => va && va.name === expression
);
if (result_idx !== -1) {
let result = this.all_scopes[result_idx];
response.body = {
result: this.parse_variable(result).value,
variablesReference: result_idx,
};
}
var variable = this.get_variable(args.expression, null, null, null);
if (variable.error == null) {
var parsed_variable = parse_variable(variable.variable);
response.body = {
result: parsed_variable.value,
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0
};
} else {
response.success = false;
response.message = variable.error;
}
}
@@ -176,76 +158,12 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments
) {
response.body = response.body || {};
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsTerminateRequest = true;
response.body.supportsEvaluateForHovers = false;
response.body.supportsStepBack = false;
response.body.supportsGotoTargetsRequest = false;
response.body.supportsCancelRequest = false;
response.body.supportsCompletionsRequest = false;
response.body.supportsFunctionBreakpoints = false;
response.body.supportsDataBreakpoints = false;
response.body.supportsBreakpointLocationsRequest = false;
response.body.supportsConditionalBreakpoints = false;
response.body.supportsHitConditionalBreakpoints = false;
response.body.supportsLogPoints = false;
response.body.supportsModulesRequest = false;
response.body.supportsReadMemoryRequest = false;
response.body.supportsRestartFrame = false;
response.body.supportsRestartRequest = false;
response.body.supportsSetExpression = false;
response.body.supportsStepInTargetsRequest = false;
response.body.supportsTerminateThreadsRequest = false;
this.sendResponse(response);
this.sendEvent(new InitializedEvent());
}
protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: LaunchRequestArguments
) {
await this.configuration_done.wait(1000);
this.debug_data.project_path = args.project;
this.exception = false;
Mediator.notify("start", [
args.project,
args.address,
args.port,
args.launch_game_instance,
args.launch_scene,
args.scene_file,
args.additional_options,
get_configuration("sceneFileConfig", "") || args.scene_file,
]);
this.sendResponse(response);
}
protected nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
) {
if (!this.exception) {
Mediator.notify("next");
this.controller.next();
this.sendResponse(response);
}
}
@@ -255,7 +173,7 @@ export class GodotDebugSession extends LoggingDebugSession {
args: DebugProtocol.PauseArguments
) {
if (!this.exception) {
Mediator.notify("break");
this.controller.break();
this.sendResponse(response);
}
}
@@ -264,10 +182,7 @@ export class GodotDebugSession extends LoggingDebugSession {
response: DebugProtocol.ScopesResponse,
args: DebugProtocol.ScopesArguments
) {
while (this.ongoing_inspections.length > 0) {
await this.got_scope.wait(100);
}
Mediator.notify("get_scopes", [args.frameId]);
this.controller.request_stack_frame_vars(args.frameId);
await this.got_scope.wait(2000);
response.body = {
@@ -284,12 +199,12 @@ export class GodotDebugSession extends LoggingDebugSession {
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments
) {
let path = (args.source.path as string).replace(/\\/g, "/");
let client_lines = args.lines || [];
const path = (args.source.path as string).replace(/\\/g, "/");
const client_lines = args.lines || [];
if (fs.existsSync(path)) {
let bps = this.debug_data.get_breakpoints(path);
let bp_lines = bps.map((bp) => bp.line);
const bp_lines = bps.map((bp) => bp.line);
bps.forEach((bp) => {
if (client_lines.indexOf(bp.line) === -1) {
@@ -298,7 +213,7 @@ export class GodotDebugSession extends LoggingDebugSession {
});
client_lines.forEach((l) => {
if (bp_lines.indexOf(l) === -1) {
let bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
if (!bp.condition) {
this.debug_data.set_breakpoint(path, l);
}
@@ -339,7 +254,7 @@ export class GodotDebugSession extends LoggingDebugSession {
column: 1,
source: new Source(
sf.file,
`${this.debug_data.project_path}/${sf.file.replace("res://", "")}`
`${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`
),
};
}),
@@ -353,7 +268,7 @@ export class GodotDebugSession extends LoggingDebugSession {
args: DebugProtocol.StepInArguments
) {
if (!this.exception) {
Mediator.notify("step");
this.controller.step();
this.sendResponse(response);
}
}
@@ -363,7 +278,7 @@ export class GodotDebugSession extends LoggingDebugSession {
args: DebugProtocol.StepOutArguments
) {
if (!this.exception) {
Mediator.notify("step_out");
this.controller.step_out();
this.sendResponse(response);
}
}
@@ -372,7 +287,10 @@ export class GodotDebugSession extends LoggingDebugSession {
response: DebugProtocol.TerminateResponse,
args: DebugProtocol.TerminateArguments
) {
Mediator.notify("stop");
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
}
this.sendResponse(response);
}
@@ -385,25 +303,32 @@ export class GodotDebugSession extends LoggingDebugSession {
response: DebugProtocol.VariablesResponse,
args: DebugProtocol.VariablesArguments
) {
let reference = this.all_scopes[args.variablesReference];
if (!this.all_scopes) {
response.body = {
variables: []
};
this.sendResponse(response);
return;
}
const reference = this.all_scopes[args.variablesReference];
let variables: DebugProtocol.Variable[];
if (!reference.sub_values) {
variables = [];
} else {
variables = reference.sub_values.map((va) => {
let sva = this.all_scopes.find(
const sva = this.all_scopes.find(
(sva) =>
sva && sva.scope_path === va.scope_path && sva.name === va.name
);
if (sva) {
return this.parse_variable(
return parse_variable(
sva,
this.all_scopes.findIndex(
(va_idx) =>
va_idx &&
va_idx.scope_path ===
`${reference.scope_path}.${reference.name}` &&
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
va_idx.name === va.name
)
);
@@ -418,83 +343,208 @@ export class GodotDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
public set_scopes(stackVars: GodotStackVars) {
this.all_scopes = [
undefined,
{
name: "local",
value: undefined,
sub_values: stackVars.locals,
scope_path: "@"
},
{
name: "member",
value: undefined,
sub_values: stackVars.members,
scope_path: "@",
},
{
name: "global",
value: undefined,
sub_values: stackVars.globals,
scope_path: "@",
},
];
stackVars.locals.forEach((va) => {
va.scope_path = "@.local";
this.append_variable(va);
});
stackVars.members.forEach((va) => {
va.scope_path = "@.member";
this.append_variable(va);
});
stackVars.globals.forEach((va) => {
va.scope_path = "@.global";
this.append_variable(va);
});
this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
public set_inspection(id: bigint, replacement: GodotVariable) {
const variables = this.all_scopes.filter(
(va) => va && va.value instanceof ObjectId && va.value.id === id
);
variables.forEach((va) => {
const index = this.all_scopes.findIndex((va_id) => va_id === va);
const old = this.all_scopes.splice(index, 1);
replacement.name = old[0].name;
replacement.scope_path = old[0].scope_path;
this.append_variable(replacement, index);
});
this.ongoing_inspections.splice(
this.ongoing_inspections.findIndex((va_id) => va_id === id),
1
);
this.previous_inspections.push(id);
// this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
private add_to_inspections() {
this.all_scopes.forEach((va) => {
if (va && va.value instanceof ObjectId) {
if (
!this.ongoing_inspections.find((va_id) => va_id === va.value.id) &&
!this.previous_inspections.find((va_id) => va_id === va.value.id)
!this.ongoing_inspections.includes(va.value.id) &&
!this.previous_inspections.includes(va.value.id)
) {
Mediator.notify("inspect_object", [va.value.id]);
this.controller.request_inspect_object(va.value.id);
this.ongoing_inspections.push(va.value.id);
}
}
});
}
protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } {
var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null };
if (!root) {
if (!expression.includes("self")) {
expression = "self." + expression;
}
root = this.all_scopes.find(x => x && x.name == "self");
object_id = this.all_scopes.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
}
var items = expression.split(".");
var propertyName = items[index + 1];
var path = items.slice(0, index + 1).join(".")
.split("self.").join("")
.split("self").join("")
.split("[").join(".")
.split("]").join("");
if (items.length == 1 && items[0] == "self") {
propertyName = "self";
}
// Detect index/key
var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
if (key) {
key = key.replace(/['"]+/g, "");
propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join("");
if (path) path += ".";
path += propertyName;
propertyName = key;
}
function sanitizeName(name: string) {
return name.split("Members/").join("").split("Locals/").join("");
}
function sanitizeScopePath(scope_path: string) {
return scope_path.split("@.member.self.").join("")
.split("@.member.self").join("")
.split("@.member.").join("")
.split("@.member").join("")
.split("@.local.").join("")
.split("@.local").join("")
.split("Locals/").join("")
.split("Members/").join("")
.split("@").join("");
}
var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) {
return {
sanitized: {
name: sanitizeName(x.name),
scope_path: sanitizeScopePath(x.scope_path)
},
real: x
};
});
result.variable = sanitized_all_scopes
.find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path)
?.real;
if (!result.variable) {
result.error = `Could not find: ${propertyName}`;
return result;
}
if (root.value.entries) {
if (result.variable.name == "self") {
result.object_id = this.all_scopes
.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
} else if (key) {
var collection = path.split(".")[path.split(".").length - 1];
var collection_items = Array.from(root.value.entries())
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == collection)[1];
result.object_id = collection_items.get
? collection_items.get(key)?.id
: collection_items[key]?.id;
} else {
result.object_id = Array.from(root.value.entries())
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName)[1].id;
}
}
if (!result.object_id) {
result.object_id = object_id;
}
result.index = this.all_scopes.findIndex(x => x && x.name == result.variable.name && x.scope_path == result.variable.scope_path);
if (items.length > 2 && index < items.length - 2) {
result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
}
return result;
}
private append_variable(variable: GodotVariable, index?: number) {
if (index) {
this.all_scopes.splice(index, 0, variable);
} else {
this.all_scopes.push(variable);
}
let base_path = `${variable.scope_path}.${variable.name}`;
const base_path = `${variable.scope_path}.${variable.name}`;
if (variable.sub_values) {
variable.sub_values.forEach((va, i) => {
va.scope_path = `${base_path}`;
va.scope_path = base_path;
this.append_variable(va, index ? index + i + 1 : undefined);
});
}
}
private parse_variable(va: GodotVariable, i?: number) {
let value = va.value;
let rendered_value = "";
let reference = 0;
let array_size = 0;
let array_type = undefined;
if (typeof value === "number") {
if (Number.isInteger(value)) {
rendered_value = `${value}`;
} else {
rendered_value = `${parseFloat(value.toFixed(5))}`;
}
} else if (
typeof value === "bigint" ||
typeof value === "boolean" ||
typeof value === "string"
) {
rendered_value = `${value}`;
} else if (typeof value === "undefined") {
rendered_value = "null";
} else {
if (Array.isArray(value)) {
rendered_value = `Array[${value.length}]`;
array_size = value.length;
array_type = "indexed";
reference = i ? i : 0;
} else if (value instanceof Map) {
if (value instanceof RawObject) {
rendered_value = `${value.class_name}`;
} else {
rendered_value = `Dictionary[${value.size}]`;
}
array_size = value.size;
array_type = "named";
reference = i ? i : 0;
} else {
rendered_value = `${value.type_name()}${value.stringify_value()}`;
reference = i ? i : 0;
}
}
return {
name: va.name,
value: rendered_value,
variablesReference: reference,
array_size: array_size > 0 ? array_size : undefined,
filter: array_type,
};
}
}

View File

@@ -0,0 +1,116 @@
import { GodotVariable, RawObject } from "../debug_runtime";
import { SceneNode } from "../scene_tree_provider";
export function parse_next_scene_node(params: any[], ofs: { offset: number } = { offset: 0 }): SceneNode {
const child_count: number = params[ofs.offset++];
const name: string = params[ofs.offset++];
const class_name: string = params[ofs.offset++];
const id: number = params[ofs.offset++];
const children: SceneNode[] = [];
for (let i = 0; i < child_count; ++i) {
children.push(parse_next_scene_node(params, ofs));
}
return new SceneNode(name, class_name, id, children);
}
export function split_buffers(buffer: Buffer) {
let len = buffer.byteLength;
let offset = 0;
const buffers: Buffer[] = [];
while (len > 0) {
const subLength = buffer.readUInt32LE(offset) + 4;
buffers.push(buffer.subarray(offset, offset + subLength));
offset += subLength;
len -= subLength;
}
return buffers;
}
export function is_variable_built_in_type(va: GodotVariable) {
var type = typeof va.value;
return ["number", "bigint", "boolean", "string"].some(x => x == type);
}
export function build_sub_values(va: GodotVariable) {
const value = va.value;
let subValues: GodotVariable[] = undefined;
if (value && Array.isArray(value)) {
subValues = value.map((va, i) => {
return { name: `${i}`, value: va } as GodotVariable;
});
} else if (value instanceof Map) {
subValues = Array.from(value.keys()).map((va) => {
if (typeof va["stringify_value"] === "function") {
return {
name: `${va.type_name()}${va.stringify_value()}`,
value: value.get(va),
} as GodotVariable;
} else {
return {
name: `${va}`,
value: value.get(va),
} as GodotVariable;
}
});
} else if (value && typeof value["sub_values"] === "function") {
subValues = value.sub_values().map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});
}
va.sub_values = subValues;
subValues?.forEach((sva) => build_sub_values(sva));
}
export function parse_variable(va: GodotVariable, i?: number) {
const value = va.value;
let rendered_value = "";
let reference = 0;
let array_size = 0;
let array_type = undefined;
if (typeof value === "number") {
if (Number.isInteger(value)) {
rendered_value = `${value}`;
} else {
rendered_value = `${parseFloat(value.toFixed(5))}`;
}
} else if (
typeof value === "bigint" ||
typeof value === "boolean" ||
typeof value === "string"
) {
rendered_value = `${value}`;
} else if (typeof value === "undefined") {
rendered_value = "null";
} else {
if (Array.isArray(value)) {
rendered_value = `Array[${value.length}]`;
array_size = value.length;
array_type = "indexed";
reference = i ? i : 0;
} else if (value instanceof Map) {
rendered_value = value["class_name"] ?? `Dictionary[${value.size}]`;
array_size = value.size;
array_type = "named";
reference = i ? i : 0;
} else {
rendered_value = `${value.type_name()}${value.stringify_value()}`;
reference = i ? i : 0;
}
}
return {
name: va.name,
value: rendered_value,
variablesReference: reference,
array_size: array_size > 0 ? array_size : undefined,
filter: array_type,
};
}

View File

@@ -0,0 +1,523 @@
import * as fs from "fs";
import net = require("net");
import { debug, window } from "vscode";
import { execSync } from "child_process";
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { VariantEncoder } from "./variables/variant_encoder";
import { VariantDecoder } from "./variables/variant_decoder";
import { RawObject } from "./variables/variants";
import { GodotStackFrame, GodotStackVars } from "../debug_runtime";
import { GodotDebugSession } from "./debug_session";
import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers";
import { get_configuration, get_free_port, projectVersion } from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { subProcess, killSubProcesses } from "../../utils/subspawn";
import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger";
import { createLogger } from "../../logger";
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
const socketLog = createLogger("debugger.socket");
class Command {
public command: string = "";
public paramCount: number = -1;
public parameters: any[] = [];
public complete: boolean = false;
public threadId: number = 0;
}
export class ServerController {
private commandBuffer: Buffer[] = [];
private encoder = new VariantEncoder();
private decoder = new VariantDecoder();
private draining = false;
private exception = "";
private server?: net.Server;
private socket?: net.Socket;
private steppingOut = false;
private currentCommand: Command = undefined;
private didFirstOutput: boolean = false;
private connectedVersion = "";
public constructor(
public session: GodotDebugSession
) { }
public break() {
this.send_command("break");
}
public continue() {
this.send_command("continue");
}
public next() {
this.send_command("next");
}
public step() {
this.send_command("step");
}
public step_out() {
this.steppingOut = true;
this.send_command("next");
}
public set_breakpoint(path_to: string, line: number) {
this.send_command("breakpoint", [path_to, line, true]);
}
public remove_breakpoint(path_to: string, line: number) {
this.session.debug_data.remove_breakpoint(path_to, line);
this.send_command("breakpoint", [path_to, line, false]);
}
public request_inspect_object(object_id: bigint) {
this.send_command("inspect_object", [object_id]);
}
public request_scene_tree() {
this.send_command("request_scene_tree");
}
public request_stack_dump() {
this.send_command("get_stack_dump");
}
public request_stack_frame_vars(frame_id: number) {
this.send_command("get_stack_frame_vars", [frame_id]);
}
public set_object_property(objectId: bigint, label: string, newParsedValue: any) {
this.send_command("set_object_property", [
objectId,
label,
newParsedValue,
]);
}
public set_exception(exception: string) {
this.exception = exception;
}
private start_game(args: LaunchRequestArguments) {
log.info("Starting game process");
const settingName = "editorPath.godot3";
const godotPath: string = get_configuration(settingName);
try {
log.info(`Verifying version of '${godotPath}'`);
const output = execSync(`${godotPath} --version`).toString().trim();
const pattern = /([34])\.([0-9]+)\.(?:[0-9]+\.)?\w+.\w+.[0-9a-f]{9}/;
const match = output.match(pattern);
if (!match) {
const message = `Cannot launch debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
log.warn(message);
prompt_for_godot_executable(message, settingName);
this.abort();
return;
}
log.info(`Got version string: '${output}'`);
this.connectedVersion = output;
if (match[1] !== settingName.slice(-1)) {
const message = `Cannot launch debug session: The current project uses Godot '${projectVersion}', but the specified Godot executable is version '${match[0]}'`;
log.warn(message);
prompt_for_godot_executable(message, settingName);
this.abort();
return;
}
} catch {
const message = `Cannot launch debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
log.warn(message);
prompt_for_godot_executable(message, settingName);
this.abort();
return;
}
let command = `"${godotPath}" --path "${args.project}"`;
const address = args.address.replace("tcp://", "");
command += ` --remote-debug "${address}:${args.port}"`;
if (get_configuration("debugger.forceVisibleCollisionShapes")) {
command += " --debug-collisions";
}
if (get_configuration("debugger.forceVisibleNavMesh")) {
command += " --debug-navigation";
}
if (args.scene && args.scene !== "main") {
log.info(`Custom scene argument provided: ${args.scene}`);
let filename = args.scene;
if (args.scene === "current") {
let path = window.activeTextEditor.document.fileName;
if (path.endsWith(".gd")) {
path = path.replace(".gd", ".tscn");
if (!fs.existsSync(path)) {
const message = `Can't find associated scene file for ${path}`;
log.warn(message);
window.showErrorMessage(message, "Ok");
this.abort();
return;
}
}
filename = path;
}
if (args.scene === "pinned") {
if (!pinnedScene) {
const message = "No pinned scene found";
log.warn(message);
window.showErrorMessage(message, "Ok");
this.abort();
return;
}
let path = pinnedScene.fsPath;
if (path.endsWith(".gd")) {
path = path.replace(".gd", ".tscn");
if (!fs.existsSync(path)) {
const message = `Can't find associated scene file for ${path}`;
log.warn(message);
window.showErrorMessage(message, "Ok");
this.abort();
return;
}
}
filename = path;
}
command += ` "${filename}"`;
}
command += this.session.debug_data.get_breakpoint_string();
if (args.additional_options) {
command += " " + args.additional_options;
}
log.info(`Launching game process using command: '${command}'`);
const debugProcess = subProcess("debug", command, { shell: true });
debugProcess.stdout.on("data", (data) => { });
debugProcess.stderr.on("data", (data) => { });
debugProcess.on("close", (code) => { });
}
private stash: Buffer;
private on_data(buffer: Buffer) {
if (this.stash) {
buffer = Buffer.concat([this.stash, buffer]);
this.stash = undefined;
}
const buffers = split_buffers(buffer);
while (buffers.length > 0) {
const chunk = buffers.shift();
const data = this.decoder.get_dataset(chunk)?.slice(1);
if (data === undefined) {
this.stash = Buffer.alloc(chunk.length);
chunk.copy(this.stash);
return;
}
this.parse_message(data);
}
}
public async launch(args: LaunchRequestArguments) {
log.info("Starting debug controller in 'launch' mode");
this.server = net.createServer((socket) => {
this.socket = socket;
socket.on("data", this.on_data.bind(this));
socket.on("close", (had_error) => {
// log.debug("socket close");
this.abort();
});
socket.on("end", () => {
// log.debug("socket end");
this.abort();
});
socket.on("error", (error) => {
// log.debug("socket error");
// this.session.sendEvent(new TerminatedEvent());
// this.stop();
});
socket.on("drain", () => {
// log.debug("socket drain");
socket.resume();
this.draining = false;
this.send_buffer();
});
});
if (args.port === -1) {
args.port = await get_free_port();
}
this.server.listen(args.port, args.address);
this.start_game(args);
}
public async attach(args: AttachRequestArguments) {
log.info("Starting debug controller in 'attach' mode");
this.server = net.createServer((socket) => {
this.socket = socket;
socket.on("data", this.on_data.bind(this));
socket.on("close", (had_error) => {
// log.debug("socket close");
// this.session.sendEvent(new TerminatedEvent());
// this.stop();
});
socket.on("end", () => {
// log.debug("socket end");
// this.session.sendEvent(new TerminatedEvent());
// this.stop();
});
socket.on("error", (error) => {
// log.error("socket error", error);
});
socket.on("drain", () => {
// log.debug("socket drain");
socket.resume();
this.draining = false;
this.send_buffer();
});
});
this.server.listen(args.port, args.address);
}
private parse_message(dataset: any[]) {
if (!this.currentCommand || this.currentCommand.complete) {
this.currentCommand = new Command();
this.currentCommand.command = dataset.shift();
}
while (dataset && dataset.length > 0) {
if (this.currentCommand.paramCount === -1) {
this.currentCommand.paramCount = dataset.shift();
} else {
this.currentCommand.parameters.push(dataset.shift());
}
if (this.currentCommand.paramCount === this.currentCommand.parameters.length) {
this.currentCommand.complete = true;
}
}
if (this.currentCommand.complete) {
socketLog.debug("rx:", [this.currentCommand.command, ...this.currentCommand.parameters]);
this.handle_command(this.currentCommand);
}
}
private handle_command(command: Command) {
switch (command.command) {
case "debug_enter": {
const reason: string = command.parameters[1];
if (reason !== "Breakpoint") {
this.set_exception(reason);
} else {
this.set_exception("");
}
this.request_stack_dump();
break;
}
case "debug_exit":
break;
case "message:click_ctrl":
// TODO: what is this?
break;
case "performance":
// TODO: what is this?
break;
case "message:scene_tree": {
const tree = parse_next_scene_node(command.parameters);
this.session.sceneTree.fill_tree(tree);
break;
}
case "message:inspect_object": {
const id = BigInt(command.parameters[0]);
const className: string = command.parameters[1];
const properties: any[] = command.parameters[2];
const rawObject = new RawObject(className);
properties.forEach((prop) => {
rawObject.set(prop[0], prop[5]);
});
const inspectedVariable = { name: "", value: rawObject };
build_sub_values(inspectedVariable);
if (this.session.inspect_callbacks.has(BigInt(id))) {
this.session.inspect_callbacks.get(BigInt(id))(
inspectedVariable.name,
inspectedVariable
);
this.session.inspect_callbacks.delete(BigInt(id));
}
this.session.set_inspection(id, inspectedVariable);
break;
}
case "stack_dump": {
const frames: GodotStackFrame[] = command.parameters.map((sf, i) => {
return {
id: i,
file: sf.get("file"),
function: sf.get("function"),
line: sf.get("line"),
};
});
this.trigger_breakpoint(frames);
this.request_scene_tree();
break;
}
case "stack_frame_vars": {
this.do_stack_frame_vars(command.parameters);
break;
}
case "output": {
if (!this.didFirstOutput) {
this.didFirstOutput = true;
// this.request_scene_tree();
}
command.parameters.forEach((line) => {
debug.activeDebugConsole.appendLine(line[0]);
});
break;
}
}
}
public abort() {
log.info("Aborting debug controller");
this.session.sendEvent(new TerminatedEvent());
this.stop();
}
public stop() {
log.info("Stopping debug controller");
killSubProcesses("debug");
this.socket?.destroy();
this.server?.close((error) => {
if (error) {
log.error(error);
}
this.server.unref();
this.server = undefined;
});
}
public trigger_breakpoint(stackFrames: GodotStackFrame[]) {
let continueStepping = false;
const stackCount = stackFrames.length;
if (stackCount === 0) {
// Engine code is being executed, no user stack trace
this.session.debug_data.last_frames = [];
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
return;
}
const file = stackFrames[0].file.replace("res://", `${this.session.debug_data.projectPath}/`);
const line = stackFrames[0].line;
if (this.steppingOut) {
const breakpoint = this.session.debug_data
.get_breakpoints(file)
.find((bp) => bp.line === line);
if (!breakpoint) {
if (this.session.debug_data.stack_count > 1) {
continueStepping = this.session.debug_data.stack_count === stackCount;
} else {
const fileSame =
stackFrames[0].file === this.session.debug_data.last_frame.file;
const funcSame =
stackFrames[0].function === this.session.debug_data.last_frame.function;
const lineGreater =
stackFrames[0].line >= this.session.debug_data.last_frame.line;
continueStepping = fileSame && funcSame && lineGreater;
}
}
}
this.session.debug_data.stack_count = stackCount;
this.session.debug_data.last_frame = stackFrames[0];
this.session.debug_data.last_frames = stackFrames;
if (continueStepping) {
this.next();
return;
}
this.steppingOut = false;
this.session.debug_data.stack_files = stackFrames.map((sf) => {
return sf.file;
});
if (this.exception.length === 0) {
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
} else {
this.session.set_exception(true);
this.session.sendEvent(
new StoppedEvent("exception", 0, this.exception)
);
}
}
private send_command(command: string, parameters: any[] = []) {
const commandArray: any[] = [command, ...parameters];
socketLog.debug("tx:", commandArray);
const buffer = this.encoder.encode_variant(commandArray);
this.commandBuffer.push(buffer);
this.send_buffer();
}
private send_buffer() {
if (!this.socket) {
return;
}
while (!this.draining && this.commandBuffer.length > 0) {
const command = this.commandBuffer.shift();
this.draining = !this.socket.write(command);
}
}
private do_stack_frame_vars(parameters: any[]) {
const stackVars = new GodotStackVars();
let localsRemaining = parameters[0];
let membersRemaining = parameters[1 + (localsRemaining * 2)];
let globalsRemaining = parameters[2 + ((localsRemaining + membersRemaining) * 2)];
let i = 1;
while (localsRemaining--) {
stackVars.locals.push({ name: parameters[i++], value: parameters[i++] });
}
i++;
while (membersRemaining--) {
stackVars.members.push({ name: parameters[i++], value: parameters[i++] });
}
i++;
while (globalsRemaining--) {
stackVars.globals.push({ name: parameters[i++], value: parameters[i++] });
}
stackVars.forEach(item => build_sub_values(item));
this.session.set_scopes(stackVars);
}
}

View File

@@ -18,7 +18,7 @@ import {
export class VariantDecoder {
public decode_variant(model: BufferModel) {
let type = this.decode_UInt32(model);
const type = this.decode_UInt32(model);
switch (type & 0xff) {
case GDScriptTypes.BOOL:
return this.decode_UInt32(model) !== 0;
@@ -87,18 +87,21 @@ export class VariantDecoder {
}
}
public get_dataset(buffer: Buffer, offset: number) {
let len = buffer.readUInt32LE(offset);
let model: BufferModel = {
public get_dataset(buffer: Buffer) {
const len = buffer.readUInt32LE(0);
if (buffer.length != len + 4) {
return undefined;
}
const model: BufferModel = {
buffer: buffer,
offset: offset + 4,
offset: 4, // data starts after the initial length
len: len,
};
let output = [];
const output = [];
output.push(len + 4);
do {
let value = this.decode_variant(model);
const value = this.decode_variant(model);
output.push(value);
} while (model.len > 0);
@@ -110,12 +113,12 @@ export class VariantDecoder {
}
private decode_Array(model: BufferModel) {
let output: Array<any> = [];
const output: Array<any> = [];
let count = this.decode_UInt32(model);
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {
let value = this.decode_variant(model);
const value = this.decode_variant(model);
output.push(value);
}
@@ -131,19 +134,19 @@ export class VariantDecoder {
}
private decode_Color(model: BufferModel) {
let rgb = this.decode_Vector3(model);
let a = this.decode_Float(model);
const rgb = this.decode_Vector3(model);
const a = this.decode_Float(model);
return new Color(rgb.x, rgb.y, rgb.z, a);
}
private decode_Dictionary(model: BufferModel) {
let output = new Map<any, any>();
const output = new Map<any, any>();
let count = this.decode_UInt32(model);
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {
let key = this.decode_variant(model);
let value = this.decode_variant(model);
const key = this.decode_variant(model);
const value = this.decode_variant(model);
output.set(key, value);
}
@@ -151,7 +154,7 @@ export class VariantDecoder {
}
private decode_Double(model: BufferModel) {
let d = model.buffer.readDoubleLE(model.offset);
const d = model.buffer.readDoubleLE(model.offset);
model.offset += 8;
model.len -= 8;
@@ -160,7 +163,7 @@ export class VariantDecoder {
}
private decode_Float(model: BufferModel) {
let f = model.buffer.readFloatLE(model.offset);
const f = model.buffer.readFloatLE(model.offset);
model.offset += 4;
model.len -= 4;
@@ -169,41 +172,53 @@ export class VariantDecoder {
}
private decode_Int32(model: BufferModel) {
let u = model.buffer.readInt32LE(model.offset);
const result = model.buffer.readInt32LE(model.offset);
model.len -= 4;
model.offset += 4;
return u;
return result;
}
private decode_UInt32(model: BufferModel) {
const result = model.buffer.readUInt32LE(model.offset);
model.len -= 4;
model.offset += 4;
return result;
}
private decode_Int64(model: BufferModel) {
let hi = model.buffer.readInt32LE(model.offset);
let lo = model.buffer.readInt32LE(model.offset + 4);
let u: BigInt = BigInt((hi << 32) | lo);
const result = model.buffer.readBigInt64LE(model.offset);
model.len -= 8;
model.offset += 8;
return u;
return result;
}
private decode_UInt64(model: BufferModel) {
const result = model.buffer.readBigUInt64LE(model.offset);
model.len -= 8;
model.offset += 8;
return result;
}
private decode_NodePath(model: BufferModel) {
let name_count = this.decode_UInt32(model) & 0x7fffffff;
const name_count = this.decode_UInt32(model) & 0x7fffffff;
let subname_count = this.decode_UInt32(model);
let flags = this.decode_UInt32(model);
let is_absolute = (flags & 1) === 1;
const flags = this.decode_UInt32(model);
const is_absolute = (flags & 1) === 1;
if (flags & 2) {
//Obsolete format with property separate from subPath
subname_count++;
}
let total = name_count + subname_count;
let names: string[] = [];
let sub_names: string[] = [];
const total = name_count + subname_count;
const names: string[] = [];
const sub_names: string[] = [];
for (let i = 0; i < total; i++) {
let str = this.decode_String(model);
const str = this.decode_String(model);
if (i < name_count) {
names.push(str);
} else {
@@ -215,13 +230,13 @@ export class VariantDecoder {
}
private decode_Object(model: BufferModel) {
let class_name = this.decode_String(model);
let prop_count = this.decode_UInt32(model);
let output = new RawObject(class_name);
const class_name = this.decode_String(model);
const prop_count = this.decode_UInt32(model);
const output = new RawObject(class_name);
for (let i = 0; i < prop_count; i++) {
let name = this.decode_String(model);
let value = this.decode_variant(model);
const name = this.decode_String(model);
const value = this.decode_variant(model);
output.set(name, value);
}
@@ -229,23 +244,23 @@ export class VariantDecoder {
}
private decode_Object_id(model: BufferModel) {
let id = this.decode_UInt64(model);
const id = this.decode_UInt64(model);
return new ObjectId(id);
}
private decode_Plane(model: BufferModel) {
let x = this.decode_Float(model);
let y = this.decode_Float(model);
let z = this.decode_Float(model);
let d = this.decode_Float(model);
const x = this.decode_Float(model);
const y = this.decode_Float(model);
const z = this.decode_Float(model);
const d = this.decode_Float(model);
return new Plane(x, y, z, d);
}
private decode_PoolByteArray(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: number[] = [];
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(model.buffer.readUInt8(model.offset));
model.offset++;
@@ -256,8 +271,8 @@ export class VariantDecoder {
}
private decode_PoolColorArray(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: Color[] = [];
const count = this.decode_UInt32(model);
const output: Color[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Color(model));
}
@@ -266,8 +281,8 @@ export class VariantDecoder {
}
private decode_PoolFloatArray(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: number[] = [];
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Float(model));
}
@@ -276,8 +291,8 @@ export class VariantDecoder {
}
private decode_PoolIntArray(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: number[] = [];
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Int32(model));
}
@@ -286,8 +301,8 @@ export class VariantDecoder {
}
private decode_PoolStringArray(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: string[] = [];
const count = this.decode_UInt32(model);
const output: string[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_String(model));
}
@@ -296,8 +311,8 @@ export class VariantDecoder {
}
private decode_PoolVector2Array(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: Vector2[] = [];
const count = this.decode_UInt32(model);
const output: Vector2[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Vector2(model));
}
@@ -306,8 +321,8 @@ export class VariantDecoder {
}
private decode_PoolVector3Array(model: BufferModel) {
let count = this.decode_UInt32(model);
let output: Vector3[] = [];
const count = this.decode_UInt32(model);
const output: Vector3[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Vector3(model));
}
@@ -316,10 +331,10 @@ export class VariantDecoder {
}
private decode_Quat(model: BufferModel) {
let x = this.decode_Float(model);
let y = this.decode_Float(model);
let z = this.decode_Float(model);
let w = this.decode_Float(model);
const x = this.decode_Float(model);
const y = this.decode_Float(model);
const z = this.decode_Float(model);
const w = this.decode_Float(model);
return new Quat(x, y, z, w);
}
@@ -335,7 +350,7 @@ export class VariantDecoder {
pad = 4 - (len % 4);
}
let str = model.buffer.toString("utf8", model.offset, model.offset + len);
const str = model.buffer.toString("utf8", model.offset, model.offset + len);
len += pad;
model.offset += len;
@@ -356,36 +371,17 @@ export class VariantDecoder {
);
}
private decode_UInt32(model: BufferModel) {
let u = model.buffer.readUInt32LE(model.offset);
model.len -= 4;
model.offset += 4;
return u;
}
private decode_UInt64(model: BufferModel) {
let hi = model.buffer.readUInt32LE(model.offset);
let lo = model.buffer.readUInt32LE(model.offset + 4);
let u = BigInt((hi << 32) | lo);
model.len -= 8;
model.offset += 8;
return u;
}
private decode_Vector2(model: BufferModel) {
let x = this.decode_Float(model);
let y = this.decode_Float(model);
const x = this.decode_Float(model);
const y = this.decode_Float(model);
return new Vector2(x, y);
}
private decode_Vector3(model: BufferModel) {
let x = this.decode_Float(model);
let y = this.decode_Float(model);
let z = this.decode_Float(model);
const x = this.decode_Float(model);
const y = this.decode_Float(model);
const z = this.decode_Float(model);
return new Vector3(x, y, z);
}

View File

@@ -35,8 +35,8 @@ export class VariantEncoder {
}
if (!model) {
let size = this.size_variant(value);
let buffer = Buffer.alloc(size + 4);
const size = this.size_variant(value);
const buffer = Buffer.alloc(size + 4);
model = {
buffer: buffer,
offset: 0,
@@ -48,7 +48,7 @@ export class VariantEncoder {
switch (typeof value) {
case "number":
{
let is_integer = Number.isInteger(value);
const is_integer = Number.isInteger(value);
if (is_integer) {
this.encode_UInt32(GDScriptTypes.INT, model);
this.encode_UInt32(value, model);
@@ -123,7 +123,7 @@ export class VariantEncoder {
}
private encode_Array(arr: any[], model: BufferModel) {
let size = arr.length;
const size = arr.length;
this.encode_UInt32(size, model);
arr.forEach((e) => {
this.encode_variant(e, model);
@@ -148,11 +148,11 @@ export class VariantEncoder {
}
private encode_Dictionary(dict: Map<any, any>, model: BufferModel) {
let size = dict.size;
const size = dict.size;
this.encode_UInt32(size, model);
let keys = Array.from(dict.keys());
const keys = Array.from(dict.keys());
keys.forEach((key) => {
let value = dict.get(key);
const value = dict.get(key);
this.encode_variant(key, model);
this.encode_variant(value, model);
});
@@ -217,11 +217,8 @@ export class VariantEncoder {
}
private encode_UInt64(value: bigint, model: BufferModel) {
let hi = Number(value >> BigInt(32));
let lo = Number(value);
this.encode_UInt32(lo, model);
this.encode_UInt32(hi, model);
model.buffer.writeBigUInt64LE(value, model.offset);
model.offset += 8;
}
private encode_Vector2(value: Vector2, model: BufferModel) {
@@ -241,9 +238,9 @@ export class VariantEncoder {
private size_Dictionary(dict: Map<any, any>): number {
let size = this.size_UInt32();
let keys = Array.from(dict.keys());
const keys = Array.from(dict.keys());
keys.forEach((key) => {
let value = dict.get(key);
const value = dict.get(key);
size += this.size_variant(key);
size += this.size_variant(value);
});

View File

@@ -1,4 +1,4 @@
import { GodotVariable } from "../debug_runtime";
import { GodotVariable } from "../../debug_runtime";
export enum GDScriptTypes {
NIL,

View File

@@ -0,0 +1,550 @@
import * as fs from "fs";
import {
LoggingDebugSession,
InitializedEvent,
Thread,
Source,
Breakpoint,
StoppedEvent,
TerminatedEvent,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { debug } from "vscode";
import { Subject } from "await-notify";
import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime";
import { LaunchRequestArguments, AttachRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ObjectId } from "./variables/variants";
import { parse_variable, is_variable_built_in_type } from "./helpers";
import { ServerController } from "./server_controller";
import { createLogger } from "../../logger";
const log = createLogger("debugger.session", { output: "Godot Debugger" });
export class GodotDebugSession extends LoggingDebugSession {
private all_scopes: GodotVariable[];
public controller = new ServerController(this);
public debug_data = new GodotDebugData(this);
public sceneTree: SceneTreeProvider;
private exception = false;
private got_scope: Subject = new Subject();
private ongoing_inspections: bigint[] = [];
private previous_inspections: bigint[] = [];
private configuration_done: Subject = new Subject();
private mode: "launch" | "attach" | "" = "";
public inspect_callbacks: Map<
bigint,
(class_name: string, variable: GodotVariable) => void
> = new Map();
public constructor() {
super();
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);
}
public dispose() {
this.controller.stop();
}
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments
) {
response.body = response.body || {};
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsTerminateRequest = true;
response.body.supportsEvaluateForHovers = false;
response.body.supportsStepBack = false;
response.body.supportsGotoTargetsRequest = false;
response.body.supportsCancelRequest = false;
response.body.supportsCompletionsRequest = false;
response.body.supportsFunctionBreakpoints = false;
response.body.supportsDataBreakpoints = false;
response.body.supportsBreakpointLocationsRequest = false;
response.body.supportsConditionalBreakpoints = false;
response.body.supportsHitConditionalBreakpoints = false;
response.body.supportsLogPoints = false;
response.body.supportsModulesRequest = false;
response.body.supportsReadMemoryRequest = false;
response.body.supportsRestartFrame = false;
response.body.supportsRestartRequest = false;
response.body.supportsSetExpression = false;
response.body.supportsStepInTargetsRequest = false;
response.body.supportsTerminateThreadsRequest = false;
this.sendResponse(response);
this.sendEvent(new InitializedEvent());
}
protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: LaunchRequestArguments
) {
await this.configuration_done.wait(1000);
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
}
protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: AttachRequestArguments
) {
await this.configuration_done.wait(1000);
this.mode = "attach";
this.exception = false;
await this.controller.attach(args);
this.sendResponse(response);
}
public configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments
) {
this.configuration_done.notify();
this.sendResponse(response);
}
protected continueRequest(
response: DebugProtocol.ContinueResponse,
args: DebugProtocol.ContinueArguments
) {
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
}
protected async evaluateRequest(
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments
) {
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
if (this.all_scopes) {
var variable = this.get_variable(args.expression, null, null, null);
if (variable.error == null) {
var parsed_variable = parse_variable(variable.variable);
response.body = {
result: parsed_variable.value,
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0
};
} else {
response.success = false;
response.message = variable.error;
}
}
if (!response.body) {
response.body = {
result: "null",
variablesReference: 0,
};
}
this.sendResponse(response);
}
protected nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
) {
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
}
protected pauseRequest(
response: DebugProtocol.PauseResponse,
args: DebugProtocol.PauseArguments
) {
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
}
protected async scopesRequest(
response: DebugProtocol.ScopesResponse,
args: DebugProtocol.ScopesArguments
) {
this.controller.request_stack_frame_vars(args.frameId);
await this.got_scope.wait(2000);
response.body = {
scopes: [
{ name: "Locals", variablesReference: 1, expensive: false },
{ name: "Members", variablesReference: 2, expensive: false },
{ name: "Globals", variablesReference: 3, expensive: false },
],
};
this.sendResponse(response);
}
protected setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments
) {
const path = (args.source.path as string).replace(/\\/g, "/");
const client_lines = args.lines || [];
if (fs.existsSync(path)) {
let bps = this.debug_data.get_breakpoints(path);
const bp_lines = bps.map((bp) => bp.line);
bps.forEach((bp) => {
if (client_lines.indexOf(bp.line) === -1) {
this.debug_data.remove_breakpoint(path, bp.line);
}
});
client_lines.forEach((l) => {
if (bp_lines.indexOf(l) === -1) {
const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l));
if (!bp.condition) {
this.debug_data.set_breakpoint(path, l);
}
}
});
bps = this.debug_data.get_breakpoints(path);
// Sort to ensure breakpoints aren't out-of-order, which would confuse VS Code.
bps.sort((a, b) => (a.line < b.line ? -1 : 1));
response.body = {
breakpoints: bps.map((bp) => {
return new Breakpoint(
true,
bp.line,
1,
new Source(bp.file.split("/").reverse()[0], bp.file)
);
}),
};
this.sendResponse(response);
}
}
protected stackTraceRequest(
response: DebugProtocol.StackTraceResponse,
args: DebugProtocol.StackTraceArguments
) {
if (this.debug_data.last_frame) {
response.body = {
totalFrames: this.debug_data.last_frames.length,
stackFrames: this.debug_data.last_frames.map((sf) => {
return {
id: sf.id,
name: sf.function,
line: sf.line,
column: 1,
source: new Source(
sf.file,
`${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`
),
};
}),
};
}
this.sendResponse(response);
}
protected stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments
) {
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
}
protected stepOutRequest(
response: DebugProtocol.StepOutResponse,
args: DebugProtocol.StepOutArguments
) {
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
}
protected terminateRequest(
response: DebugProtocol.TerminateResponse,
args: DebugProtocol.TerminateArguments
) {
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
}
this.sendResponse(response);
}
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
response.body = { threads: [new Thread(0, "thread_1")] };
this.sendResponse(response);
}
protected async variablesRequest(
response: DebugProtocol.VariablesResponse,
args: DebugProtocol.VariablesArguments
) {
if (!this.all_scopes) {
response.body = {
variables: []
};
this.sendResponse(response);
return;
}
const reference = this.all_scopes[args.variablesReference];
let variables: DebugProtocol.Variable[];
if (!reference.sub_values) {
variables = [];
} else {
variables = reference.sub_values.map((va) => {
const sva = this.all_scopes.find(
(sva) =>
sva && sva.scope_path === va.scope_path && sva.name === va.name
);
if (sva) {
return parse_variable(
sva,
this.all_scopes.findIndex(
(va_idx) =>
va_idx &&
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
va_idx.name === va.name
)
);
}
});
}
response.body = {
variables: variables,
};
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
public set_scopes(stackVars: GodotStackVars) {
this.all_scopes = [
undefined,
{
name: "local",
value: undefined,
sub_values: stackVars.locals,
scope_path: "@"
},
{
name: "member",
value: undefined,
sub_values: stackVars.members,
scope_path: "@",
},
{
name: "global",
value: undefined,
sub_values: stackVars.globals,
scope_path: "@",
},
];
stackVars.locals.forEach((va) => {
va.scope_path = "@.local";
this.append_variable(va);
});
stackVars.members.forEach((va) => {
va.scope_path = "@.member";
this.append_variable(va);
});
stackVars.globals.forEach((va) => {
va.scope_path = "@.global";
this.append_variable(va);
});
this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
public set_inspection(id: bigint, replacement: GodotVariable) {
const variables = this.all_scopes.filter(
(va) => va && va.value instanceof ObjectId && va.value.id === id
);
variables.forEach((va) => {
const index = this.all_scopes.findIndex((va_id) => va_id === va);
const old = this.all_scopes.splice(index, 1);
replacement.name = old[0].name;
replacement.scope_path = old[0].scope_path;
this.append_variable(replacement, index);
});
this.ongoing_inspections.splice(
this.ongoing_inspections.findIndex((va_id) => va_id === id),
1
);
this.previous_inspections.push(id);
// this.add_to_inspections();
if (this.ongoing_inspections.length === 0) {
this.previous_inspections = [];
this.got_scope.notify();
}
}
private add_to_inspections() {
this.all_scopes.forEach((va) => {
if (va && va.value instanceof ObjectId) {
if (
!this.ongoing_inspections.includes(va.value.id) &&
!this.previous_inspections.includes(va.value.id)
) {
this.controller.request_inspect_object(va.value.id);
this.ongoing_inspections.push(va.value.id);
}
}
});
}
protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } {
var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null };
if (!root) {
if (!expression.includes("self")) {
expression = "self." + expression;
}
root = this.all_scopes.find(x => x && x.name == "self");
object_id = this.all_scopes.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
}
var items = expression.split(".");
var propertyName = items[index + 1];
var path = items.slice(0, index + 1).join(".")
.split("self.").join("")
.split("self").join("")
.split("[").join(".")
.split("]").join("");
if (items.length == 1 && items[0] == "self") {
propertyName = "self";
}
// Detect index/key
var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
if (key) {
key = key.replace(/['"]+/g, "");
propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join("");
if (path) path += ".";
path += propertyName;
propertyName = key;
}
function sanitizeName(name: string) {
return name.split("Members/").join("").split("Locals/").join("");
}
function sanitizeScopePath(scope_path: string) {
return scope_path.split("@.member.self.").join("")
.split("@.member.self").join("")
.split("@.member.").join("")
.split("@.member").join("")
.split("@.local.").join("")
.split("@.local").join("")
.split("Locals/").join("")
.split("Members/").join("")
.split("@").join("");
}
var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) {
return {
sanitized: {
name: sanitizeName(x.name),
scope_path: sanitizeScopePath(x.scope_path)
},
real: x
};
});
result.variable = sanitized_all_scopes
.find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path)
?.real;
if (!result.variable) {
result.error = `Could not find: ${propertyName}`;
return result;
}
if (root.value.entries) {
if (result.variable.name == "self") {
result.object_id = this.all_scopes
.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value;
} else if (key) {
var collection = path.split(".")[path.split(".").length - 1];
var collection_items = Array.from(root.value.entries())
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == collection)[1];
result.object_id = collection_items.get
? collection_items.get(key)?.id
: collection_items[key]?.id;
} else {
result.object_id = Array.from(root.value.entries())
.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName)[1].id;
}
}
if (!result.object_id) {
result.object_id = object_id;
}
result.index = this.all_scopes.findIndex(x => x && x.name == result.variable.name && x.scope_path == result.variable.scope_path);
if (items.length > 2 && index < items.length - 2) {
result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
}
return result;
}
private append_variable(variable: GodotVariable, index?: number) {
if (index) {
this.all_scopes.splice(index, 0, variable);
} else {
this.all_scopes.push(variable);
}
const base_path = `${variable.scope_path}.${variable.name}`;
if (variable.sub_values) {
variable.sub_values.forEach((va, i) => {
va.scope_path = base_path;
this.append_variable(va, index ? index + i + 1 : undefined);
});
}
}
}

View File

@@ -0,0 +1,123 @@
import { GodotVariable, RawObject } from "../debug_runtime";
import { SceneNode } from "../scene_tree_provider";
import { createLogger } from "../../logger";
const log = createLogger("debugger.helpers");
export function parse_next_scene_node(params: any[], ofs: { offset: number } = { offset: 0 }): SceneNode {
const child_count: number = params[ofs.offset++];
const name: string = params[ofs.offset++];
const class_name: string = params[ofs.offset++];
const id: number = params[ofs.offset++];
const scene_file_path: string = params[ofs.offset++];
const view_flags: number = params[ofs.offset++];
const children: SceneNode[] = [];
for (let i = 0; i < child_count; ++i) {
children.push(parse_next_scene_node(params, ofs));
}
return new SceneNode(name, class_name, id, children, scene_file_path, view_flags);
}
export function split_buffers(buffer: Buffer) {
let len = buffer.byteLength;
let offset = 0;
const buffers: Buffer[] = [];
while (len > 0) {
const subLength = buffer.readUInt32LE(offset) + 4;
buffers.push(buffer.subarray(offset, offset + subLength));
offset += subLength;
len -= subLength;
}
return buffers;
}
export function is_variable_built_in_type(va: GodotVariable) {
var type = typeof va.value;
return ["number", "bigint", "boolean", "string"].some(x => x == type);
}
export function build_sub_values(va: GodotVariable) {
const value = va.value;
let subValues: GodotVariable[] = undefined;
if (value && Array.isArray(value)) {
subValues = value.map((va, i) => {
return { name: `${i}`, value: va } as GodotVariable;
});
} else if (value instanceof Map) {
subValues = Array.from(value.keys()).map((va) => {
if (typeof va["stringify_value"] === "function") {
return {
name: `${va.type_name()}${va.stringify_value()}`,
value: value.get(va),
} as GodotVariable;
} else {
return {
name: `${va}`,
value: value.get(va),
} as GodotVariable;
}
});
} else if (value && typeof value["sub_values"] === "function") {
subValues = value.sub_values().map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});
}
va.sub_values = subValues;
subValues?.forEach(build_sub_values);
}
export function parse_variable(va: GodotVariable, i?: number) {
const value = va.value;
let rendered_value = "";
let reference = 0;
let array_size = 0;
let array_type = undefined;
if (typeof value === "number") {
if (Number.isInteger(value)) {
rendered_value = `${value}`;
} else {
rendered_value = `${parseFloat(value.toFixed(5))}`;
}
} else if (
typeof value === "bigint" ||
typeof value === "boolean" ||
typeof value === "string"
) {
rendered_value = `${value}`;
} else if (typeof value === "undefined") {
rendered_value = "null";
} else {
if (Array.isArray(value)) {
rendered_value = `Array[${value.length}]`;
array_size = value.length;
array_type = "indexed";
reference = i ? i : 0;
} else if (value instanceof Map) {
rendered_value = value["class_name"] ?? `Dictionary[${value.size}]`;
array_size = value.size;
array_type = "named";
reference = i ? i : 0;
} else {
rendered_value = `${value.type_name()}${value.stringify_value()}`;
reference = i ? i : 0;
}
}
return {
name: va.name,
value: rendered_value,
variablesReference: reference,
array_size: array_size > 0 ? array_size : undefined,
filter: array_type,
};
}

View File

@@ -0,0 +1,529 @@
import * as fs from "fs";
import net = require("net");
import { debug, window } from "vscode";
import { execSync } from "child_process";
import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter";
import { VariantEncoder } from "./variables/variant_encoder";
import { VariantDecoder } from "./variables/variant_decoder";
import { RawObject } from "./variables/variants";
import { GodotStackFrame, GodotVariable, GodotStackVars } from "../debug_runtime";
import { GodotDebugSession } from "./debug_session";
import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers";
import { get_configuration, get_free_port, projectVersion } from "../../utils";
import { prompt_for_godot_executable } from "../../utils/prompts";
import { subProcess, killSubProcesses } from "../../utils/subspawn";
import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger";
import { createLogger } from "../../logger";
const log = createLogger("debugger.controller", { output: "Godot Debugger" });
const socketLog = createLogger("debugger.socket");
class Command {
public command: string = "";
public paramCount: number = -1;
public parameters: any[] = [];
public complete: boolean = false;
public threadId: number = 0;
}
export class ServerController {
private commandBuffer: Buffer[] = [];
private encoder = new VariantEncoder();
private decoder = new VariantDecoder();
private draining = false;
private exception = "";
private threadId: number;
private server?: net.Server;
private socket?: net.Socket;
private steppingOut = false;
private didFirstOutput: boolean = false;
private partialStackVars = new GodotStackVars();
private connectedVersion = "";
public constructor(
public session: GodotDebugSession
) { }
public break() {
this.send_command("break");
}
public continue() {
this.send_command("continue");
}
public next() {
this.send_command("next");
}
public step() {
this.send_command("step");
}
public step_out() {
this.steppingOut = true;
this.send_command("next");
}
public set_breakpoint(path_to: string, line: number) {
this.send_command("breakpoint", [path_to, line, true]);
}
public remove_breakpoint(path_to: string, line: number) {
this.session.debug_data.remove_breakpoint(path_to, line);
this.send_command("breakpoint", [path_to, line, false]);
}
public request_inspect_object(object_id: bigint) {
this.send_command("scene:inspect_object", [object_id]);
}
public request_scene_tree() {
this.send_command("scene:request_scene_tree");
}
public request_stack_dump() {
this.send_command("get_stack_dump");
}
public request_stack_frame_vars(frame_id: number) {
this.send_command("get_stack_frame_vars", [frame_id]);
}
public set_object_property(objectId: bigint, label: string, newParsedValue: any) {
this.send_command("scene:set_object_property", [
objectId,
label,
newParsedValue,
]);
}
public set_exception(exception: string) {
this.exception = exception;
}
private start_game(args: LaunchRequestArguments) {
log.info("Starting game process");
const settingName = "editorPath.godot4";
const godotPath: string = get_configuration(settingName);
try {
log.info(`Verifying version of '${godotPath}'`);
const output = execSync(`${godotPath} --version`).toString().trim();
const pattern = /([34])\.([0-9]+)\.(?:[0-9]+\.)?\w+.\w+.[0-9a-f]{9}/;
const match = output.match(pattern);
if (!match) {
const message = `Cannot launch debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
log.warn(message);
prompt_for_godot_executable(message, settingName);
this.abort();
return;
}
log.info(`Got version string: '${output}'`);
this.connectedVersion = output;
if (match[1] !== settingName.slice(-1)) {
const message = `Cannot launch debug session: The current project uses Godot '${projectVersion}', but the specified Godot executable is version '${match[0]}'`;
log.warn(message);
prompt_for_godot_executable(message, settingName);
this.abort();
return;
}
} catch {
const message = `Cannot launch debug session: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
log.warn(message);
prompt_for_godot_executable(message, settingName);
this.abort();
return;
}
let command = `"${godotPath}" --path "${args.project}"`;
const address = args.address.replace("tcp://", "");
command += ` --remote-debug "tcp://${address}:${args.port}"`;
if (get_configuration("debugger.forceVisibleCollisionShapes")) {
command += " --debug-collisions";
}
if (get_configuration("debugger.forceVisibleNavMesh")) {
command += " --debug-navigation";
}
if (args.scene && args.scene !== "main") {
log.info(`Custom scene argument provided: ${args.scene}`);
let filename = args.scene;
if (args.scene === "current") {
let path = window.activeTextEditor.document.fileName;
if (path.endsWith(".gd")) {
path = path.replace(".gd", ".tscn");
if (!fs.existsSync(path)) {
const message = `Can't find associated scene file for ${path}`;
log.warn(message);
window.showErrorMessage(message, "Ok");
this.abort();
return;
}
}
filename = path;
}
if (args.scene === "pinned") {
if (!pinnedScene) {
const message = "No pinned scene found";
log.warn(message);
window.showErrorMessage(message, "Ok");
this.abort();
return;
}
let path = pinnedScene.fsPath;
if (path.endsWith(".gd")) {
path = path.replace(".gd", ".tscn");
if (!fs.existsSync(path)) {
const message = `Can't find associated scene file for ${path}`;
log.warn(message);
window.showErrorMessage(message, "Ok");
this.abort();
return;
}
}
filename = path;
}
command += ` "${filename}"`;
}
command += this.session.debug_data.get_breakpoint_string();
if (args.additional_options) {
command += " " + args.additional_options;
}
log.info(`Launching game process using command: '${command}'`);
const debugProcess = subProcess("debug", command, { shell: true });
debugProcess.stdout.on("data", (data) => { });
debugProcess.stderr.on("data", (data) => { });
debugProcess.on("close", (code) => { });
}
private stash: Buffer;
private on_data(buffer: Buffer) {
if (this.stash) {
buffer = Buffer.concat([this.stash, buffer]);
this.stash = undefined;
}
const buffers = split_buffers(buffer);
while (buffers.length > 0) {
const chunk = buffers.shift();
const data = this.decoder.get_dataset(chunk)?.slice(1);
if (data === undefined) {
this.stash = Buffer.alloc(chunk.length);
chunk.copy(this.stash);
return;
}
socketLog.debug("rx:", data[0]);
const command = this.parse_message(data[0]);
this.handle_command(command);
}
}
public async launch(args: LaunchRequestArguments) {
log.info("Starting debug controller in 'launch' mode");
this.server = net.createServer((socket) => {
this.socket = socket;
socket.on("data", this.on_data.bind(this));
socket.on("close", (had_error) => {
// log.debug("socket close");
this.abort();
});
socket.on("end", () => {
// log.debug("socket end");
this.abort();
});
socket.on("error", (error) => {
// log.debug("socket error");
// this.session.sendEvent(new TerminatedEvent());
// this.stop();
});
socket.on("drain", () => {
// log.debug("socket drain");
socket.resume();
this.draining = false;
this.send_buffer();
});
});
if (args.port === -1) {
args.port = await get_free_port();
}
this.server.listen(args.port, args.address);
this.start_game(args);
}
public async attach(args: AttachRequestArguments) {
log.info("Starting debug controller in 'attach' mode");
this.server = net.createServer((socket) => {
this.socket = socket;
socket.on("data", this.on_data.bind(this));
socket.on("close", (had_error) => {
// log.debug("socket close");
// this.session.sendEvent(new TerminatedEvent());
// this.stop();
});
socket.on("end", () => {
// log.debug("socket end");
// this.session.sendEvent(new TerminatedEvent());
// this.stop();
});
socket.on("error", (error) => {
// log.error("socket error", error);
});
socket.on("drain", () => {
// log.debug("socket drain");
socket.resume();
this.draining = false;
this.send_buffer();
});
});
this.server.listen(args.port, args.address);
}
private parse_message(dataset: any[]) {
const command = new Command();
let i = 0;
command.command = dataset[i++];
if (this.connectedVersion[2] >= "2") {
command.threadId = dataset[i++];
}
command.parameters = dataset[i++];
return command;
}
private handle_command(command: Command) {
switch (command.command) {
case "debug_enter": {
const reason: string = command.parameters[1];
if (reason !== "Breakpoint") {
this.set_exception(reason);
} else {
this.set_exception("");
}
this.request_stack_dump();
break;
}
case "debug_exit":
break;
case "message:click_ctrl":
// TODO: what is this?
break;
case "performance:profile_frame":
// TODO: what is this?
break;
case "set_pid":
this.threadId = command.threadId;
break;
case "scene:scene_tree": {
const tree = parse_next_scene_node(command.parameters);
this.session.sceneTree.fill_tree(tree);
break;
}
case "scene:inspect_object": {
const id = BigInt(command.parameters[0]);
const className: string = command.parameters[1];
const properties: any[] = command.parameters[2];
const rawObject = new RawObject(className);
properties.forEach((prop) => {
rawObject.set(prop[0], prop[5]);
});
const inspectedVariable = { name: "", value: rawObject };
build_sub_values(inspectedVariable);
if (this.session.inspect_callbacks.has(BigInt(id))) {
this.session.inspect_callbacks.get(BigInt(id))(
inspectedVariable.name,
inspectedVariable
);
this.session.inspect_callbacks.delete(BigInt(id));
}
this.session.set_inspection(id, inspectedVariable);
break;
}
case "stack_dump": {
const frames: GodotStackFrame[] = [];
for (let i = 1; i < command.parameters.length; i += 3) {
frames.push({
id: frames.length,
file: command.parameters[i + 0],
line: command.parameters[i + 1],
function: command.parameters[i + 2],
});
}
this.trigger_breakpoint(frames);
this.request_scene_tree();
break;
}
case "stack_frame_vars": {
this.partialStackVars.reset(command.parameters[0]);
this.session.set_scopes(this.partialStackVars);
break;
}
case "stack_frame_var": {
this.do_stack_frame_var(
command.parameters[0],
command.parameters[1],
command.parameters[2],
command.parameters[3],
);
break;
}
case "output": {
if (!this.didFirstOutput) {
this.didFirstOutput = true;
// this.request_scene_tree();
}
debug.activeDebugConsole.appendLine(command.parameters[0]);
break;
}
}
}
public abort() {
log.info("Aborting debug controller");
this.session.sendEvent(new TerminatedEvent());
this.stop();
}
public stop() {
log.info("Stopping debug controller");
killSubProcesses("debug");
this.socket?.destroy();
this.server?.close((error) => {
if (error) {
log.error(error);
}
this.server.unref();
this.server = undefined;
});
}
public trigger_breakpoint(stackFrames: GodotStackFrame[]) {
let continueStepping = false;
const stackCount = stackFrames.length;
if (stackCount === 0) {
// Engine code is being executed, no user stack trace
this.session.debug_data.last_frames = [];
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
return;
}
const file = stackFrames[0].file.replace("res://", `${this.session.debug_data.projectPath}/`);
const line = stackFrames[0].line;
if (this.steppingOut) {
const breakpoint = this.session.debug_data
.get_breakpoints(file)
.find((bp) => bp.line === line);
if (!breakpoint) {
if (this.session.debug_data.stack_count > 1) {
continueStepping = this.session.debug_data.stack_count === stackCount;
} else {
const fileSame =
stackFrames[0].file === this.session.debug_data.last_frame.file;
const funcSame =
stackFrames[0].function === this.session.debug_data.last_frame.function;
const lineGreater =
stackFrames[0].line >= this.session.debug_data.last_frame.line;
continueStepping = fileSame && funcSame && lineGreater;
}
}
}
this.session.debug_data.stack_count = stackCount;
this.session.debug_data.last_frame = stackFrames[0];
this.session.debug_data.last_frames = stackFrames;
if (continueStepping) {
this.next();
return;
}
this.steppingOut = false;
this.session.debug_data.stack_files = stackFrames.map((sf) => {
return sf.file;
});
if (this.exception.length === 0) {
this.session.sendEvent(new StoppedEvent("breakpoint", 0));
} else {
this.session.set_exception(true);
this.session.sendEvent(
new StoppedEvent("exception", 0, this.exception)
);
}
}
private send_command(command: string, parameters?: any[]) {
const commandArray: any[] = [command];
if (this.connectedVersion[2] >= "2") {
commandArray.push(this.threadId);
}
commandArray.push(parameters ?? []);
socketLog.debug("tx:", commandArray);
const buffer = this.encoder.encode_variant(commandArray);
this.commandBuffer.push(buffer);
this.send_buffer();
}
private send_buffer() {
if (!this.socket) {
return;
}
while (!this.draining && this.commandBuffer.length > 0) {
const command = this.commandBuffer.shift();
this.draining = !this.socket.write(command);
}
}
private do_stack_frame_var(
name: string,
scope: 0 | 1 | 2, // 0 = locals, 1 = members, 2 = globals
type: bigint,
value: any,
) {
if (this.partialStackVars.remaining === 0) {
throw new Error("More stack frame variables were sent than expected.");
}
const variable: GodotVariable = { name, value, type };
build_sub_values(variable);
const scopeName = ["locals", "members", "globals"][scope];
this.partialStackVars[scopeName].push(variable);
this.partialStackVars.remaining--;
if (this.partialStackVars.remaining === 0) {
this.session.set_scopes(this.partialStackVars);
}
}
}

View File

@@ -0,0 +1,652 @@
import {
GDScriptTypes,
BufferModel,
Vector3,
Vector2,
Basis,
AABB,
Color,
NodePath,
ObjectId,
Plane,
Quat,
Rect2,
Transform3D,
Transform2D,
RawObject,
Vector2i,
Vector3i,
Rect2i,
Vector4,
Vector4i,
StringName,
Projection,
ENCODE_FLAG_64,
ENCODE_FLAG_OBJECT_AS_ID,
RID,
Callable,
Signal,
} from "./variants";
export class VariantDecoder {
public decode_variant(model: BufferModel) {
const type = this.decode_UInt32(model);
switch (type & 0xff) {
case GDScriptTypes.BOOL:
return this.decode_UInt32(model) !== 0;
case GDScriptTypes.INT:
if (type & ENCODE_FLAG_64) {
return this.decode_Int64(model);
} else {
return this.decode_Int32(model);
}
case GDScriptTypes.FLOAT:
if (type & ENCODE_FLAG_64) {
return this.decode_Float64(model);
} else {
return this.decode_Float32(model);
}
case GDScriptTypes.STRING:
return this.decode_String(model);
case GDScriptTypes.VECTOR2:
if (type & ENCODE_FLAG_64) {
return this.decode_Vector2d(model);
} else {
return this.decode_Vector2f(model);
}
case GDScriptTypes.VECTOR2I:
return this.decode_Vector2i(model);
case GDScriptTypes.RECT2:
if (type & ENCODE_FLAG_64) {
return this.decode_Rect2d(model);
} else {
return this.decode_Rect2f(model);
}
case GDScriptTypes.RECT2I:
return this.decode_Rect2i(model);
case GDScriptTypes.VECTOR3:
if (type & ENCODE_FLAG_64) {
return this.decode_Vector3d(model);
} else {
return this.decode_Vector3f(model);
}
case GDScriptTypes.VECTOR3I:
return this.decode_Vector3i(model);
case GDScriptTypes.TRANSFORM2D:
if (type & ENCODE_FLAG_64) {
return this.decode_Transform2Dd(model);
} else {
return this.decode_Transform2Df(model);
}
case GDScriptTypes.PLANE:
if (type & ENCODE_FLAG_64) {
return this.decode_Planed(model);
} else {
return this.decode_Planef(model);
}
case GDScriptTypes.VECTOR4:
if (type & ENCODE_FLAG_64) {
return this.decode_Vector4d(model);
} else {
return this.decode_Vector4f(model);
}
case GDScriptTypes.VECTOR4I:
return this.decode_Vector4i(model);
case GDScriptTypes.QUATERNION:
if (type & ENCODE_FLAG_64) {
return this.decode_Quaterniond(model);
} else {
return this.decode_Quaternionf(model);
}
case GDScriptTypes.AABB:
if (type & ENCODE_FLAG_64) {
return this.decode_AABBd(model);
} else {
return this.decode_AABBf(model);
}
case GDScriptTypes.BASIS:
if (type & ENCODE_FLAG_64) {
return this.decode_Basisd(model);
} else {
return this.decode_Basisf(model);
}
case GDScriptTypes.TRANSFORM3D:
if (type & ENCODE_FLAG_64) {
return this.decode_Transform3Dd(model);
} else {
return this.decode_Transform3Df(model);
}
case GDScriptTypes.PROJECTION:
if (type & ENCODE_FLAG_64) {
return this.decode_Projectiond(model);
} else {
return this.decode_Projectionf(model);
}
case GDScriptTypes.COLOR:
return this.decode_Color(model);
case GDScriptTypes.STRING_NAME:
return this.decode_StringName(model);
case GDScriptTypes.NODE_PATH:
return this.decode_NodePath(model);
case GDScriptTypes.RID:
return this.decode_RID(model);
case GDScriptTypes.OBJECT:
if (type & ENCODE_FLAG_OBJECT_AS_ID) {
return this.decode_Object_id(model);
} else {
return this.decode_Object(model);
}
case GDScriptTypes.CALLABLE:
return this.decode_Callable(model);
case GDScriptTypes.SIGNAL:
return this.decode_Signal(model);
case GDScriptTypes.DICTIONARY:
return this.decode_Dictionary(model);
case GDScriptTypes.ARRAY:
return this.decode_Array(model);
case GDScriptTypes.PACKED_BYTE_ARRAY:
return this.decode_PackedByteArray(model);
case GDScriptTypes.PACKED_INT32_ARRAY:
return this.decode_PackedInt32Array(model);
case GDScriptTypes.PACKED_INT64_ARRAY:
return this.decode_PackedInt64Array(model);
case GDScriptTypes.PACKED_FLOAT32_ARRAY:
return this.decode_PackedFloat32Array(model);
case GDScriptTypes.PACKED_FLOAT64_ARRAY:
return this.decode_PackedFloat32Array(model);
case GDScriptTypes.PACKED_STRING_ARRAY:
return this.decode_PackedStringArray(model);
case GDScriptTypes.PACKED_VECTOR2_ARRAY:
if (type & ENCODE_FLAG_OBJECT_AS_ID) {
return this.decode_PackedVector2dArray(model);
} else {
return this.decode_PackedVector2fArray(model);
}
case GDScriptTypes.PACKED_VECTOR3_ARRAY:
if (type & ENCODE_FLAG_OBJECT_AS_ID) {
return this.decode_PackedVector3dArray(model);
} else {
return this.decode_PackedVector3fArray(model);
}
case GDScriptTypes.PACKED_COLOR_ARRAY:
return this.decode_PackedColorArray(model);
default:
return undefined;
}
}
public get_dataset(buffer: Buffer) {
const len = buffer.readUInt32LE(0);
if (buffer.length != len + 4) {
return undefined;
}
const model: BufferModel = {
buffer: buffer,
offset: 4, // data starts after the initial length
len: len,
};
const output = [];
output.push(len + 4);
do {
const value = this.decode_variant(model);
if (value === undefined) {
throw new Error("Unable to decode variant.");
}
output.push(value);
} while (model.len > 0);
return output;
}
private decode_AABBf(model: BufferModel) {
return new AABB(this.decode_Vector3f(model), this.decode_Vector3f(model));
}
private decode_AABBd(model: BufferModel) {
return new AABB(this.decode_Vector3d(model), this.decode_Vector3d(model));
}
private decode_Array(model: BufferModel) {
const output: Array<any> = [];
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {
const value = this.decode_variant(model);
output.push(value);
}
return output;
}
private decode_Basisf(model: BufferModel) {
return new Basis(
this.decode_Vector3f(model),
this.decode_Vector3f(model),
this.decode_Vector3f(model)
);
}
private decode_Basisd(model: BufferModel) {
return new Basis(
this.decode_Vector3d(model),
this.decode_Vector3d(model),
this.decode_Vector3d(model)
);
}
private decode_Color(model: BufferModel) {
const rgb = this.decode_Vector3f(model);
const a = this.decode_Float32(model);
return new Color(rgb.x, rgb.y, rgb.z, a);
}
private decode_Dictionary(model: BufferModel) {
const output = new Map<any, any>();
const count = this.decode_UInt32(model);
for (let i = 0; i < count; i++) {
const key = this.decode_variant(model);
const value = this.decode_variant(model);
output.set(key, value);
}
return output;
}
private decode_Float32(model: BufferModel) {
const f = model.buffer.readFloatLE(model.offset);
model.offset += 4;
model.len -= 4;
return f; // + (f < 0 ? -1e-10 : 1e-10);
}
private decode_Float64(model: BufferModel) {
const f = model.buffer.readDoubleLE(model.offset);
model.offset += 8;
model.len -= 8;
return f; // + (f < 0 ? -1e-10 : 1e-10);
}
private decode_Int32(model: BufferModel) {
const result = model.buffer.readInt32LE(model.offset);
model.len -= 4;
model.offset += 4;
return result;
}
private decode_UInt32(model: BufferModel) {
const result = model.buffer.readUInt32LE(model.offset);
model.len -= 4;
model.offset += 4;
return result;
}
private decode_Int64(model: BufferModel) {
const result = model.buffer.readBigInt64LE(model.offset);
model.len -= 8;
model.offset += 8;
return result;
}
private decode_UInt64(model: BufferModel) {
const result = model.buffer.readBigUInt64LE(model.offset);
model.len -= 8;
model.offset += 8;
return result;
}
private decode_NodePath(model: BufferModel) {
const name_count = this.decode_UInt32(model) & 0x7fffffff;
let subname_count = this.decode_UInt32(model);
const flags = this.decode_UInt32(model);
const is_absolute = (flags & 1) === 1;
if (flags & 2) {
//Obsolete format with property separate from subPath
subname_count++;
}
const total = name_count + subname_count;
const names: string[] = [];
const sub_names: string[] = [];
for (let i = 0; i < total; i++) {
const str = this.decode_String(model);
if (i < name_count) {
names.push(str);
} else {
sub_names.push(str);
}
}
return new NodePath(names, sub_names, is_absolute);
}
private decode_Object(model: BufferModel) {
const class_name = this.decode_String(model);
const prop_count = this.decode_UInt32(model);
const output = new RawObject(class_name);
for (let i = 0; i < prop_count; i++) {
const name = this.decode_String(model);
const value = this.decode_variant(model);
output.set(name, value);
}
return output;
}
private decode_Object_id(model: BufferModel) {
const id = this.decode_UInt64(model);
return new ObjectId(id);
}
private decode_RID(model: BufferModel) {
const id = this.decode_UInt64(model);
return new RID(id);
}
private decode_Callable(model: BufferModel) {
return new Callable();
}
private decode_Signal(model: BufferModel) {
return new Signal(this.decode_String(model), this.decode_Object_id(model));
}
private decode_Planef(model: BufferModel) {
const x = this.decode_Float32(model);
const y = this.decode_Float32(model);
const z = this.decode_Float32(model);
const d = this.decode_Float32(model);
return new Plane(x, y, z, d);
}
private decode_Planed(model: BufferModel) {
const x = this.decode_Float64(model);
const y = this.decode_Float64(model);
const z = this.decode_Float64(model);
const d = this.decode_Float64(model);
return new Plane(x, y, z, d);
}
private decode_PackedByteArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(model.buffer.readUInt8(model.offset));
model.offset++;
model.len--;
}
return output;
}
private decode_PackedColorArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: Color[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Color(model));
}
return output;
}
private decode_PackedFloat32Array(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Float32(model));
}
return output;
}
private decode_PackedFloat64Array(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Float64(model));
}
return output;
}
private decode_PackedInt32Array(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: number[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Int32(model));
}
return output;
}
private decode_PackedInt64Array(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: bigint[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Int64(model));
}
return output;
}
private decode_PackedStringArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: string[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_String(model));
}
return output;
}
private decode_PackedVector2fArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: Vector2[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Vector2f(model));
}
return output;
}
private decode_PackedVector3fArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: Vector3[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Vector3f(model));
}
return output;
}
private decode_PackedVector2dArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: Vector2[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Vector2d(model));
}
return output;
}
private decode_PackedVector3dArray(model: BufferModel) {
const count = this.decode_UInt32(model);
const output: Vector3[] = [];
for (let i = 0; i < count; i++) {
output.push(this.decode_Vector3d(model));
}
return output;
}
private decode_Quaternionf(model: BufferModel) {
const x = this.decode_Float32(model);
const y = this.decode_Float32(model);
const z = this.decode_Float32(model);
const w = this.decode_Float32(model);
return new Quat(x, y, z, w);
}
private decode_Quaterniond(model: BufferModel) {
const x = this.decode_Float64(model);
const y = this.decode_Float64(model);
const z = this.decode_Float64(model);
const w = this.decode_Float64(model);
return new Quat(x, y, z, w);
}
private decode_Rect2f(model: BufferModel) {
return new Rect2(this.decode_Vector2f(model), this.decode_Vector2f(model));
}
private decode_Rect2d(model: BufferModel) {
return new Rect2(this.decode_Vector2d(model), this.decode_Vector2d(model));
}
private decode_Rect2i(model: BufferModel) {
return new Rect2i(this.decode_Vector2f(model), this.decode_Vector2f(model));
}
private decode_String(model: BufferModel) {
let len = this.decode_UInt32(model);
let pad = 0;
if (len % 4 !== 0) {
pad = 4 - (len % 4);
}
const str = model.buffer.toString("utf8", model.offset, model.offset + len);
len += pad;
model.offset += len;
model.len -= len;
return str;
}
private decode_StringName(model: BufferModel) {
return new StringName(this.decode_String(model));
}
private decode_Transform3Df(model: BufferModel) {
return new Transform3D(this.decode_Basisf(model), this.decode_Vector3f(model));
}
private decode_Transform3Dd(model: BufferModel) {
return new Transform3D(this.decode_Basisd(model), this.decode_Vector3d(model));
}
private decode_Projectionf(model: BufferModel) {
return new Projection(this.decode_Vector4f(model), this.decode_Vector4f(model), this.decode_Vector4f(model), this.decode_Vector4f(model));
}
private decode_Projectiond(model: BufferModel) {
return new Projection(this.decode_Vector4d(model), this.decode_Vector4d(model), this.decode_Vector4d(model), this.decode_Vector4d(model));
}
private decode_Transform2Df(model: BufferModel) {
return new Transform2D(
this.decode_Vector2f(model),
this.decode_Vector2f(model),
this.decode_Vector2f(model)
);
}
private decode_Transform2Dd(model: BufferModel) {
return new Transform2D(
this.decode_Vector2d(model),
this.decode_Vector2d(model),
this.decode_Vector2d(model)
);
}
private decode_Vector2f(model: BufferModel) {
const x = this.decode_Float32(model);
const y = this.decode_Float32(model);
return new Vector2(x, y);
}
private decode_Vector2d(model: BufferModel) {
const x = this.decode_Float64(model);
const y = this.decode_Float64(model);
return new Vector2(x, y);
}
private decode_Vector2i(model: BufferModel) {
const x = this.decode_Int32(model);
const y = this.decode_Int32(model);
return new Vector2i(x, y);
}
private decode_Vector3f(model: BufferModel) {
const x = this.decode_Float32(model);
const y = this.decode_Float32(model);
const z = this.decode_Float32(model);
return new Vector3(x, y, z);
}
private decode_Vector3d(model: BufferModel) {
const x = this.decode_Float64(model);
const y = this.decode_Float64(model);
const z = this.decode_Float64(model);
return new Vector3(x, y, z);
}
private decode_Vector3i(model: BufferModel) {
const x = this.decode_Int32(model);
const y = this.decode_Int32(model);
const z = this.decode_Int32(model);
return new Vector3i(x, y, z);
}
private decode_Vector4f(model: BufferModel) {
const x = this.decode_Float32(model);
const y = this.decode_Float32(model);
const z = this.decode_Float32(model);
const w = this.decode_Float32(model);
return new Vector4(x, y, z, w);
}
private decode_Vector4d(model: BufferModel) {
const x = this.decode_Float64(model);
const y = this.decode_Float64(model);
const z = this.decode_Float64(model);
const w = this.decode_Float64(model);
return new Vector4(x, y, z, w);
}
private decode_Vector4i(model: BufferModel) {
const x = this.decode_Int32(model);
const y = this.decode_Int32(model);
const z = this.decode_Int32(model);
const w = this.decode_Int32(model);
return new Vector4i(x, y, z, w);
}
}

View File

@@ -0,0 +1,446 @@
import {
GDScriptTypes,
BufferModel,
Vector3,
Vector2,
Basis,
AABB,
Color,
Plane,
Quat,
Rect2,
Transform3D,
Transform2D,
Vector3i,
Vector2i,
Rect2i,
Vector4i,
Vector4,
StringName,
Projection,
ENCODE_FLAG_64,
} from "./variants";
export class VariantEncoder {
public encode_variant(
value:
| number
| bigint
| boolean
| string
| Map<any, any>
| Array<any>
| object
| undefined,
model?: BufferModel
) {
if (
typeof value === "number" &&
Number.isInteger(value) &&
(value > 2147483647 || value < -2147483648)
) {
value = BigInt(value);
}
if (!model) {
const size = this.size_variant(value);
const buffer = Buffer.alloc(size + 4);
model = {
buffer: buffer,
offset: 0,
len: 0,
};
this.encode_UInt32(size, model);
}
switch (typeof value) {
case "number":
{
const is_integer = Number.isInteger(value);
if (is_integer) {
this.encode_UInt32(GDScriptTypes.INT, model);
this.encode_UInt32(value, model);
} else {
this.encode_UInt32(GDScriptTypes.FLOAT, model);
this.encode_Float32(value, model);
}
}
break;
case "bigint":
this.encode_UInt32(GDScriptTypes.INT | ENCODE_FLAG_64, model);
this.encode_UInt64(value, model);
break;
case "boolean":
this.encode_UInt32(GDScriptTypes.BOOL, model);
this.encode_Bool(value, model);
break;
case "string":
this.encode_UInt32(GDScriptTypes.STRING, model);
this.encode_String(value, model);
break;
case "undefined":
break;
default:
if (Array.isArray(value)) {
this.encode_UInt32(GDScriptTypes.ARRAY, model);
this.encode_Array(value, model);
} else if (value instanceof Map) {
this.encode_UInt32(GDScriptTypes.DICTIONARY, model);
this.encode_Dictionary(value, model);
} else {
if (value instanceof Vector2i) {
this.encode_UInt32(GDScriptTypes.VECTOR2I, model);
this.encode_Vector2i(value, model);
} else if (value instanceof Vector2) {
this.encode_UInt32(GDScriptTypes.VECTOR2, model);
this.encode_Vector2(value, model);
} else if (value instanceof Rect2i) {
this.encode_UInt32(GDScriptTypes.RECT2I, model);
this.encode_Rect2i(value, model);
} else if (value instanceof Rect2) {
this.encode_UInt32(GDScriptTypes.RECT2, model);
this.encode_Rect2(value, model);
} else if (value instanceof Vector3i) {
this.encode_UInt32(GDScriptTypes.VECTOR3I, model);
this.encode_Vector3i(value, model);
} else if (value instanceof Vector3) {
this.encode_UInt32(GDScriptTypes.VECTOR3, model);
this.encode_Vector3(value, model);
} else if (value instanceof Vector4i) {
this.encode_UInt32(GDScriptTypes.VECTOR4I, model);
this.encode_Vector4i(value, model);
} else if (value instanceof Vector4) {
this.encode_UInt32(GDScriptTypes.VECTOR4, model);
this.encode_Vector4(value, model);
} else if (value instanceof Transform2D) {
this.encode_UInt32(GDScriptTypes.TRANSFORM2D, model);
this.encode_Transform2D(value, model);
} else if (value instanceof StringName) {
this.encode_UInt32(GDScriptTypes.STRING_NAME, model);
this.encode_StringName(value, model);
} else if (value instanceof Plane) {
this.encode_UInt32(GDScriptTypes.PLANE, model);
this.encode_Plane(value, model);
} else if (value instanceof Projection) {
this.encode_UInt32(GDScriptTypes.PROJECTION, model);
this.encode_Projection(value, model);
} else if (value instanceof Quat) {
this.encode_UInt32(GDScriptTypes.QUATERNION, model);
this.encode_Quaternion(value, model);
} else if (value instanceof AABB) {
this.encode_UInt32(GDScriptTypes.AABB, model);
this.encode_AABB(value, model);
} else if (value instanceof Basis) {
this.encode_UInt32(GDScriptTypes.BASIS, model);
this.encode_Basis(value, model);
} else if (value instanceof Transform3D) {
this.encode_UInt32(GDScriptTypes.TRANSFORM3D, model);
this.encode_Transform3D(value, model);
} else if (value instanceof Color) {
this.encode_UInt32(GDScriptTypes.COLOR, model);
this.encode_Color(value, model);
}
}
}
return model.buffer;
}
private encode_AABB(value: AABB, model: BufferModel) {
this.encode_Vector3(value.position, model);
this.encode_Vector3(value.size, model);
}
private encode_Array(arr: any[], model: BufferModel) {
const size = arr.length;
this.encode_UInt32(size, model);
arr.forEach((e) => {
this.encode_variant(e, model);
});
}
private encode_Basis(value: Basis, model: BufferModel) {
this.encode_Vector3(value.x, model);
this.encode_Vector3(value.y, model);
this.encode_Vector3(value.z, model);
}
private encode_Bool(bool: boolean, model: BufferModel) {
this.encode_UInt32(bool ? 1 : 0, model);
}
private encode_Color(value: Color, model: BufferModel) {
this.encode_Float32(value.r, model);
this.encode_Float32(value.g, model);
this.encode_Float32(value.b, model);
this.encode_Float32(value.a, model);
}
private encode_Dictionary(dict: Map<any, any>, model: BufferModel) {
const size = dict.size;
this.encode_UInt32(size, model);
const keys = Array.from(dict.keys());
keys.forEach((key) => {
const value = dict.get(key);
this.encode_variant(key, model);
this.encode_variant(value, model);
});
}
private encode_Float64(value: number, model: BufferModel) {
model.buffer.writeDoubleLE(value, model.offset);
model.offset += 8;
}
private encode_Float32(value: number, model: BufferModel) {
model.buffer.writeFloatLE(value, model.offset);
model.offset += 4;
}
private encode_Plane(value: Plane, model: BufferModel) {
this.encode_Float32(value.x, model);
this.encode_Float32(value.y, model);
this.encode_Float32(value.z, model);
this.encode_Float32(value.d, model);
}
private encode_Quaternion(value: Quat, model: BufferModel) {
this.encode_Float32(value.x, model);
this.encode_Float32(value.y, model);
this.encode_Float32(value.z, model);
this.encode_Float32(value.w, model);
}
private encode_Rect2(value: Rect2, model: BufferModel) {
this.encode_Vector2(value.position, model);
this.encode_Vector2(value.size, model);
}
private encode_Rect2i(value: Rect2i, model: BufferModel) {
this.encode_Vector2i(value.position, model);
this.encode_Vector2i(value.size, model);
}
private encode_String(str: string, model: BufferModel) {
let str_len = str.length;
this.encode_UInt32(str_len, model);
model.buffer.write(str, model.offset, str_len, "utf8");
model.offset += str_len;
str_len += 4;
while (str_len % 4) {
str_len++;
model.buffer.writeUInt8(0, model.offset);
model.offset++;
}
}
private encode_Transform3D(value: Transform3D, model: BufferModel) {
this.encode_Basis(value.basis, model);
this.encode_Vector3(value.origin, model);
}
private encode_Transform2D(value: Transform2D, model: BufferModel) {
this.encode_Vector2(value.origin, model);
this.encode_Vector2(value.x, model);
this.encode_Vector2(value.y, model);
}
private encode_Projection(value: Projection, model: BufferModel) {
this.encode_Vector4(value.x, model);
this.encode_Vector4(value.y, model);
this.encode_Vector4(value.z, model);
this.encode_Vector4(value.w, model);
}
private encode_UInt32(int: number, model: BufferModel) {
model.buffer.writeUInt32LE(int, model.offset);
model.offset += 4;
}
private encode_Int32(int: number, model: BufferModel) {
model.buffer.writeInt32LE(int, model.offset);
model.offset += 4;
}
private encode_UInt64(value: bigint, model: BufferModel) {
model.buffer.writeBigUInt64LE(value, model.offset);
model.offset += 8;
}
private encode_Vector2(value: Vector2, model: BufferModel) {
this.encode_Float32(value.x, model);
this.encode_Float32(value.y, model);
}
private encode_Vector3(value: Vector3, model: BufferModel) {
this.encode_Float32(value.x, model);
this.encode_Float32(value.y, model);
this.encode_Float32(value.z, model);
}
private encode_Vector4(value: Vector4, model: BufferModel) {
this.encode_Float32(value.x, model);
this.encode_Float32(value.y, model);
this.encode_Float32(value.z, model);
this.encode_Float32(value.w, model);
}
private encode_Vector2i(value: Vector2i, model: BufferModel) {
this.encode_Int32(value.x, model);
this.encode_Int32(value.y, model);
}
private encode_Vector3i(value: Vector3i, model: BufferModel) {
this.encode_Int32(value.x, model);
this.encode_Int32(value.y, model);
this.encode_Int32(value.z, model);
}
private encode_Vector4i(value: Vector4i, model: BufferModel) {
this.encode_Int32(value.x, model);
this.encode_Int32(value.y, model);
this.encode_Int32(value.z, model);
this.encode_Int32(value.w, model);
}
private encode_StringName(value: StringName, model: BufferModel) {
this.encode_String(value.value, model);
}
private size_Bool(): number {
return this.size_UInt32();
}
private size_Dictionary(dict: Map<any, any>): number {
let size = this.size_UInt32();
const keys = Array.from(dict.keys());
keys.forEach((key) => {
const value = dict.get(key);
size += this.size_variant(key);
size += this.size_variant(value);
});
return size;
}
private size_String(str: string): number {
let size = this.size_UInt32() + str.length;
while (size % 4) {
size++;
}
return size;
}
private size_UInt32(): number {
return 4;
}
private size_UInt64(): number {
return 8;
}
private size_array(arr: any[]): number {
let size = this.size_UInt32();
arr.forEach((e) => {
size += this.size_variant(e);
});
return size;
}
private size_variant(
value:
| number
| bigint
| boolean
| string
| Map<any, any>
| any[]
| object
| undefined
): number {
let size = 4;
if (
typeof value === "number" &&
(value > 2147483647 || value < -2147483648)
) {
value = BigInt(value);
}
switch (typeof value) {
case "number":
size += this.size_UInt32();
break;
case "bigint":
size += this.size_UInt64();
break;
case "boolean":
size += this.size_Bool();
break;
case "string":
size += this.size_String(value);
break;
case "undefined":
break;
default:
// TODO: size of nodepath, rid, object, callable, signal
if (Array.isArray(value)) {
size += this.size_array(value);
break;
} else if (value instanceof Map) {
size += this.size_Dictionary(value);
break;
} else if (value instanceof StringName) {
size += this.size_String(value.value);
break;
} else {
switch (value["__type__"]) {
case "Vector2":
case "Vector2i":
size += this.size_UInt32() * 2;
break;
case "Rect2":
case "Rect2i":
size += this.size_UInt32() * 4;
break;
case "Vector3":
case "Vector3i":
size += this.size_UInt32() * 3;
break;
case "Vector4":
case "Vector4i":
size += this.size_UInt32() * 4;
break;
case "Transform2D":
size += this.size_UInt32() * 6;
break;
case "Projection":
size += this.size_UInt32() * 16;
break;
case "Plane":
size += this.size_UInt32() * 4;
break;
case "Quaternion":
size += this.size_UInt32() * 4;
break;
case "AABB":
size += this.size_UInt32() * 6;
break;
case "Basis":
size += this.size_UInt32() * 9;
break;
case "Transform3D":
size += this.size_UInt32() * 12;
break;
case "Color":
size += this.size_UInt32() * 4;
break;
}
}
break;
}
return size;
}
}

View File

@@ -0,0 +1,475 @@
import { GodotVariable } from "../../debug_runtime";
export enum GDScriptTypes {
NIL,
// atomic types
BOOL,
INT,
FLOAT,
STRING,
// math types
VECTOR2,
VECTOR2I,
RECT2,
RECT2I,
VECTOR3,
VECTOR3I,
TRANSFORM2D,
VECTOR4,
VECTOR4I,
PLANE,
QUATERNION,
AABB,
BASIS,
TRANSFORM3D,
PROJECTION,
// misc types
COLOR,
STRING_NAME,
NODE_PATH,
RID,
OBJECT,
CALLABLE,
SIGNAL,
DICTIONARY,
ARRAY,
// typed arrays
PACKED_BYTE_ARRAY,
PACKED_INT32_ARRAY,
PACKED_INT64_ARRAY,
PACKED_FLOAT32_ARRAY,
PACKED_FLOAT64_ARRAY,
PACKED_STRING_ARRAY,
PACKED_VECTOR2_ARRAY,
PACKED_VECTOR3_ARRAY,
PACKED_COLOR_ARRAY,
VARIANT_MAX
}
export const ENCODE_FLAG_64 = 1 << 16;
export const ENCODE_FLAG_OBJECT_AS_ID = 1 << 16;
export interface BufferModel {
buffer: Buffer;
len: number;
offset: number;
}
export interface GDObject {
stringify_value(): string;
sub_values(): GodotVariable[];
type_name(): string;
}
function clean_number(value: number) {
return +Number.parseFloat(String(value)).toFixed(1);
}
export class Vector3 implements GDObject {
constructor(
public x: number = 0.0,
public y: number = 0.0,
public z: number = 0.0
) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${clean_number(
this.z
)})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
{ name: "z", value: this.z },
];
}
public type_name(): string {
return "Vector3";
}
}
export class Vector3i extends Vector3 {
// TODO: Truncate values in sub_values and stringify_value
public type_name(): string {
return "Vector3i";
}
}
export class Vector4 implements GDObject {
constructor(
public x: number = 0.0,
public y: number = 0.0,
public z: number = 0.0,
public w: number = 0.0
) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${
clean_number(this.z)}, ${clean_number(this.w)})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
{ name: "z", value: this.z },
{ name: "w", value: this.w },
];
}
public type_name(): string {
return "Vector4";
}
}
export class Vector4i extends Vector4 {
// TODO: Truncate values in sub_values and stringify_value
public type_name(): string {
return "Vector4i";
}
}
export class Vector2 implements GDObject {
constructor(public x: number = 0.0, public y: number = 0.0) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
];
}
public type_name(): string {
return "Vector2";
}
}
export class Vector2i extends Vector2 {
// TODO: Truncate values in sub_values and stringify_value
public type_name(): string {
return "Vector2i";
}
}
export class Basis implements GDObject {
constructor(public x: Vector3, public y: Vector3, public z: Vector3) {}
public stringify_value(): string {
return `(${this.x.stringify_value()}, ${this.y.stringify_value()}, ${this.z.stringify_value()})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
{ name: "z", value: this.z },
];
}
public type_name(): string {
return "Basis";
}
}
export class AABB implements GDObject {
constructor(public position: Vector3, public size: Vector3) {}
public stringify_value(): string {
return `(${this.position.stringify_value()}, ${this.size.stringify_value()})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "position", value: this.position },
{ name: "size", value: this.size },
];
}
public type_name(): string {
return "AABB";
}
}
export class Color implements GDObject {
constructor(
public r: number,
public g: number,
public b: number,
public a: number = 1.0
) {}
public stringify_value(): string {
return `(${clean_number(this.r)}, ${clean_number(this.g)}, ${clean_number(
this.b
)}, ${clean_number(this.a)})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "r", value: this.r },
{ name: "g", value: this.g },
{ name: "b", value: this.b },
{ name: "a", value: this.a },
];
}
public type_name(): string {
return "Color";
}
}
export class NodePath implements GDObject {
constructor(
public names: string[],
public sub_names: string[],
public absolute: boolean
) {}
public stringify_value(): string {
return `(/${this.names.join("/")}${
this.sub_names.length > 0 ? ":" : ""
}${this.sub_names.join(":")})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "names", value: this.names },
{ name: "sub_names", value: this.sub_names },
{ name: "absolute", value: this.absolute },
];
}
public type_name(): string {
return "NodePath";
}
}
export class RawObject extends Map<any, any> {
constructor(public class_name: string) {
super();
}
}
export class ObjectId implements GDObject {
constructor(public id: bigint) {}
public stringify_value(): string {
return `<${this.id}>`;
}
public sub_values(): GodotVariable[] {
return [{ name: "id", value: this.id }];
}
public type_name(): string {
return "Object";
}
}
export class RID extends ObjectId {
public type_name(): string {
return "RID";
}
}
export class Plane implements GDObject {
constructor(
public x: number,
public y: number,
public z: number,
public d: number
) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${clean_number(
this.z
)}, ${clean_number(this.d)})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
{ name: "z", value: this.z },
{ name: "d", value: this.d },
];
}
public type_name(): string {
return "Plane";
}
}
export class Quat implements GDObject {
constructor(
public x: number,
public y: number,
public z: number,
public w: number
) {}
public stringify_value(): string {
return `(${clean_number(this.x)}, ${clean_number(this.y)}, ${clean_number(
this.z
)}, ${clean_number(this.w)})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
{ name: "z", value: this.z },
{ name: "w", value: this.w },
];
}
public type_name(): string {
return "Quat";
}
}
export class Rect2 implements GDObject {
constructor(public position: Vector2, public size: Vector2) {}
public stringify_value(): string {
return `(${this.position.stringify_value()} - ${this.size.stringify_value()})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "position", value: this.position },
{ name: "size", value: this.size },
];
}
public type_name(): string {
return "Rect2";
}
}
export class Rect2i extends Rect2 {
// TODO: Truncate values in sub_values and stringify_value
public type_name(): string {
return "Rect2i";
}
}
export class Projection implements GDObject {
constructor(public x: Vector4, public y: Vector4, public z: Vector4, public w: Vector4) {}
public stringify_value(): string {
return `(${this.x.stringify_value()}, ${this.y.stringify_value()}, ${this.z.stringify_value()}, ${this.w.stringify_value()})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "x", value: this.x },
{ name: "y", value: this.y },
{ name: "z", value: this.z },
{ name: "w", value: this.w },
];
}
public type_name(): string {
return "Projection";
}
}
export class Transform3D implements GDObject {
constructor(public basis: Basis, public origin: Vector3) {}
public stringify_value(): string {
return `(${this.basis.stringify_value()} - ${this.origin.stringify_value()})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "basis", value: this.basis },
{ name: "origin", value: this.origin },
];
}
public type_name(): string {
return "Transform";
}
}
export class Transform2D implements GDObject {
constructor(public origin: Vector2, public x: Vector2, public y: Vector2) {}
public stringify_value(): string {
return `(${this.origin.stringify_value()} - (${this.x.stringify_value()}, ${this.y.stringify_value()})`;
}
public sub_values(): GodotVariable[] {
return [
{ name: "origin", value: this.origin },
{ name: "x", value: this.x },
{ name: "y", value: this.y },
];
}
public type_name(): string {
return "Transform2D";
}
}
export class StringName implements GDObject {
constructor(public value: string) {}
public stringify_value(): string {
return this.value;
}
public sub_values(): GodotVariable[] {
return [
{ name: "value", value: this.value },
];
}
public type_name(): string {
return "StringName";
}
}
export class Callable implements GDObject {
public stringify_value(): string {
return "()";
}
public sub_values(): GodotVariable[] {
return [];
}
public type_name(): string {
return "Callable";
}
}
export class Signal implements GDObject {
constructor(public name: string, public oid: ObjectId) {}
public stringify_value(): string {
return `${this.name}() ${this.oid.stringify_value()}`;
}
public sub_values(): GodotVariable[] {
return undefined;
}
public type_name(): string {
return "Signal";
}
}

View File

@@ -6,8 +6,7 @@ import {
TreeItem,
TreeItemCollapsibleState,
} from "vscode";
import { GodotVariable } from "../debug_runtime";
import { RawObject, ObjectId } from "../variables/variants";
import { GodotVariable, RawObject, ObjectId } from "./debug_runtime";
export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
private _on_did_change_tree_data: EventEmitter<
@@ -63,10 +62,10 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
property: RemoteProperty,
new_parsed_value: any
) {
let idx = parents.length - 1;
let value = parents[idx].value;
const idx = parents.length - 1;
const value = parents[idx].value;
if (Array.isArray(value)) {
let idx = parseInt(property.label);
const idx = parseInt(property.label);
if (idx < value.length) {
value[idx] = new_parsed_value;
}
@@ -98,7 +97,7 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
}
private parse_variable(va: GodotVariable, object_id?: number) {
let value = va.value;
const value = va.value;
let rendered_value = "";
if (typeof value === "number") {
@@ -132,31 +131,31 @@ export class InspectorProvider implements TreeDataProvider<RemoteProperty> {
let child_props: RemoteProperty[] = [];
if (value) {
let sub_variables =
const sub_variables =
typeof value["sub_values"] === "function" &&
value instanceof ObjectId === false
? value.sub_values()
: Array.isArray(value)
? value.map((va, i) => {
return { name: `${i}`, value: va };
})
})
: value instanceof Map
? Array.from(value.keys()).map((va) => {
let name =
const name =
typeof va["rendered_value"] === "function"
? va.rendered_value()
: `${va}`;
let map_value = value.get(va);
const map_value = value.get(va);
return { name: name, value: map_value };
})
})
: [];
child_props = sub_variables?.map((va) => {
return this.parse_variable(va, object_id);
});
}
let out_prop = new RemoteProperty(
const out_prop = new RemoteProperty(
va.name,
value,
object_id,

View File

@@ -1,263 +0,0 @@
import { ServerController } from "./server_controller";
import { window, OutputChannel } from "vscode";
import { GodotDebugSession } from "./debug_session";
import { StoppedEvent, TerminatedEvent } from "vscode-debugadapter";
import { GodotDebugData, GodotVariable } from "./debug_runtime";
export class Mediator {
private static controller?: ServerController;
private static debug_data?: GodotDebugData;
private static inspect_callbacks: Map<
number,
(class_name: string, variable: GodotVariable) => void
> = new Map();
private static session?: GodotDebugSession;
private static first_output = false;
private static output: OutputChannel = window.createOutputChannel("Godot");
private constructor() {}
public static notify(event: string, parameters: any[] = []) {
switch (event) {
case "output":
if (!this.first_output) {
this.first_output = true;
this.output.show(true);
this.output.clear();
this.controller?.send_request_scene_tree_command();
}
let lines: string[] = parameters;
lines.forEach((line) => {
let message_content: string = line[0];
//let message_kind: number = line[1];
// OutputChannel doesn't give a way to distinguish between a
// regular string (message_kind == 0) and an error string (message_kind == 1).
this.output.appendLine(message_content);
});
break;
case "continue":
this.controller?.continue();
break;
case "next":
this.controller?.next();
break;
case "step":
this.controller?.step();
break;
case "step_out":
this.controller?.step_out();
break;
case "inspect_object":
this.controller?.send_inspect_object_request(parameters[0]);
if (parameters[1]) {
this.inspect_callbacks.set(parameters[0], parameters[1]);
}
break;
case "inspected_object":
let inspected_variable = { name: "", value: parameters[1] };
this.build_sub_values(inspected_variable);
if (this.inspect_callbacks.has(Number(parameters[0]))) {
this.inspect_callbacks.get(Number(parameters[0]))(
inspected_variable.name,
inspected_variable
);
this.inspect_callbacks.delete(Number(parameters[0]));
} else {
this.session?.set_inspection(parameters[0], inspected_variable);
}
break;
case "stack_dump":
this.controller?.trigger_breakpoint(parameters);
this.controller?.send_request_scene_tree_command();
break;
case "request_scene_tree":
this.controller?.send_request_scene_tree_command();
break;
case "scene_tree":
this.debug_data?.scene_tree?.fill_tree(parameters[0]);
break;
case "get_scopes":
this.controller?.send_scope_request(parameters[0]);
break;
case "stack_frame_vars":
this.do_stack_frame_vars(parameters[0], parameters[1], parameters[2]);
break;
case "remove_breakpoint":
this.controller?.remove_breakpoint(parameters[0], parameters[1]);
break;
case "set_breakpoint":
this.controller?.set_breakpoint(parameters[0], parameters[1]);
break;
case "stopped_on_breakpoint":
this.debug_data.last_frames = parameters[0];
this.session?.sendEvent(new StoppedEvent("breakpoint", 0));
break;
case "stopped_on_exception":
this.debug_data.last_frames = parameters[0];
this.session?.set_exception(true);
this.session?.sendEvent(
new StoppedEvent("exception", 0, parameters[1])
);
break;
case "break":
this.controller?.break();
break;
case "changed_value":
this.controller?.set_object_property(
parameters[0],
parameters[1],
parameters[2]
);
break;
case "debug_enter":
let reason: string = parameters[0];
if (reason !== "Breakpoint") {
this.controller?.set_exception(reason);
} else {
this.controller?.set_exception("");
}
this.controller?.stack_dump();
break;
case "start":
this.first_output = false;
this.controller?.start(
parameters[0],
parameters[1],
parameters[2],
parameters[3],
parameters[4],
parameters[5],
parameters[6],
this.debug_data
);
break;
case "debug_exit":
break;
case "stop":
this.controller?.stop();
this.session?.sendEvent(new TerminatedEvent());
break;
case "error":
this.controller?.set_exception(parameters[0]);
this.controller?.stop();
this.session?.sendEvent(new TerminatedEvent());
break;
}
}
public static set_controller(controller: ServerController) {
this.controller = controller;
}
public static set_debug_data(debug_data: GodotDebugData) {
this.debug_data = debug_data;
}
public static set_session(debug_session: GodotDebugSession) {
this.session = debug_session;
}
private static build_sub_values(va: GodotVariable) {
let value = va.value;
let sub_values: GodotVariable[] = undefined;
if (value && Array.isArray(value)) {
sub_values = value.map((va, i) => {
return { name: `${i}`, value: va } as GodotVariable;
});
} else if (value instanceof Map) {
sub_values = Array.from(value.keys()).map((va) => {
if (typeof va["stringify_value"] === "function") {
return {
name: `${va.type_name()}${va.stringify_value()}`,
value: value.get(va),
} as GodotVariable;
} else {
return {
name: `${va}`,
value: value.get(va),
} as GodotVariable;
}
});
} else if (value && typeof value["sub_values"] === "function") {
sub_values = value.sub_values().map((sva) => {
return { name: sva.name, value: sva.value } as GodotVariable;
});
}
va.sub_values = sub_values;
sub_values?.forEach((sva) => this.build_sub_values(sva));
}
private static do_stack_frame_vars(
locals: any[],
members: any[],
globals: any[]
) {
let locals_out: GodotVariable[] = [];
let members_out: GodotVariable[] = [];
let globals_out: GodotVariable[] = [];
for (
let i = 0;
i < locals.length + members.length + globals.length;
i += 2
) {
const name =
i < locals.length
? locals[i]
: i < members.length + locals.length
? members[i - locals.length]
: globals[i - locals.length - members.length];
const value =
i < locals.length
? locals[i + 1]
: i < members.length + locals.length
? members[i - locals.length + 1]
: globals[i - locals.length - members.length + 1];
let variable: GodotVariable = {
name: name,
value: value,
};
this.build_sub_values(variable);
i < locals.length
? locals_out.push(variable)
: i < members.length + locals.length
? members_out.push(variable)
: globals_out.push(variable);
}
this.session?.set_scopes(locals_out, members_out, globals_out);
}
}

View File

@@ -7,7 +7,6 @@ import {
TreeItemCollapsibleState,
} from "vscode";
import path = require("path");
import fs = require("fs");
export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
private _on_did_change_tree_data: EventEmitter<
@@ -18,7 +17,7 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
public readonly onDidChangeTreeData: Event<SceneNode> | undefined = this
._on_did_change_tree_data.event;
constructor() {}
constructor() { }
public fill_tree(tree: SceneNode) {
this.tree = tree;
@@ -38,9 +37,8 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
}
public getTreeItem(element: SceneNode): TreeItem | Thenable<TreeItem> {
let has_children = element.children.length > 0;
let tree_item: TreeItem | undefined;
tree_item = new TreeItem(
const has_children = element.children.length > 0;
const tree_item: TreeItem = new TreeItem(
element.label,
has_children
? element === this.tree
@@ -51,77 +49,38 @@ export class SceneTreeProvider implements TreeDataProvider<SceneNode> {
tree_item.description = element.class_name;
tree_item.iconPath = element.iconPath;
if (element.scene_file_path) {
let tooltip = "";
tooltip += `${element.label}`;
tooltip += `\n${element.class_name}`;
tooltip += `\n${element.object_id}`;
if (element.scene_file_path) {
tooltip += `\n${element.scene_file_path}`;
}
tree_item.tooltip = tooltip;
}
return tree_item;
}
}
function match_icon_to_class(class_name: string) {
let icon_name = `icon${class_name
.replace(/(2|3)D/, "$1d")
.replace(/([A-Z0-9])/g, "_$1")
.toLowerCase()}.svg`;
return icon_name;
}
export class SceneNode extends TreeItem {
constructor(
public label: string,
public class_name: string,
public object_id: number,
public children: SceneNode[],
public collapsibleState?: TreeItemCollapsibleState
public scene_file_path?: string,
public view_flags?: number,
) {
super(label, collapsibleState);
super(label);
let light = path.join(
__filename,
"..",
"..",
"..",
"..",
"resources",
"light",
match_icon_to_class(class_name)
);
if (!fs.existsSync(light)) {
path.join(
__filename,
"..",
"..",
"..",
"..",
"resources",
"light",
"node.svg"
);
}
let dark = path.join(
__filename,
"..",
"..",
"..",
"..",
"resources",
"dark",
match_icon_to_class(class_name)
);
if (!fs.existsSync(light)) {
path.join(
__filename,
"..",
"..",
"..",
"..",
"resources",
"dark",
"node.svg"
);
}
const iconDir = path.join(__filename, "..", "..", "..", "resources", "godot_icons");
const iconName = class_name + ".svg";
this.iconPath = {
light: light,
dark: dark,
light: path.join(iconDir, "light", iconName),
dark: path.join(iconDir, "dark", iconName),
};
}
}

View File

@@ -1,314 +0,0 @@
import { CommandParser } from "./commands/command_parser";
import { Mediator } from "./mediator";
import { VariantDecoder } from "./variables/variant_decoder";
import {
GodotBreakpoint,
GodotStackFrame,
GodotDebugData,
} from "./debug_runtime";
import { window } from "vscode";
const TERMINATE = require("terminate");
import net = require("net");
import utils = require("../utils");
import cp = require("child_process");
import path = require("path");
export class ServerController {
private command_buffer: Buffer[] = [];
private commands = new CommandParser();
private debug_data: GodotDebugData;
private decoder = new VariantDecoder();
private draining = false;
private exception = "";
private godot_pid: number;
private server?: net.Server;
private socket?: net.Socket;
private stepping_out = false;
private terminated = false;
public break() {
this.add_and_send(this.commands.make_break_command());
}
public continue() {
this.add_and_send(this.commands.make_continue_command());
}
public next() {
this.add_and_send(this.commands.make_next_command());
}
public remove_breakpoint(path_to: string, line: number) {
this.debug_data.remove_breakpoint(path_to, line);
this.add_and_send(
this.commands.make_remove_breakpoint_command(path_to, line)
);
}
public send_inspect_object_request(object_id: bigint) {
this.add_and_send(this.commands.make_inspect_object_command(object_id));
}
public send_request_scene_tree_command() {
this.add_and_send(this.commands.make_request_scene_tree_command());
}
public send_scope_request(frame_id: number) {
this.add_and_send(this.commands.make_stack_frame_vars_command(frame_id));
}
public set_breakpoint(path_to: string, line: number) {
this.add_and_send(
this.commands.make_send_breakpoint_command(path_to, line)
);
}
public set_exception(exception: string) {
this.exception = exception;
}
public set_object_property(
object_id: bigint,
label: string,
new_parsed_value: any
) {
this.add_and_send(
this.commands.make_set_object_value_command(
BigInt(object_id),
label,
new_parsed_value
)
);
}
public stack_dump() {
this.add_and_send(this.commands.make_stack_dump_command());
}
public start(
project_path: string,
address: string,
port: number,
launch_instance: boolean,
launch_scene: boolean,
scene_file: string | undefined,
additional_options: string | undefined,
debug_data: GodotDebugData
) {
this.debug_data = debug_data;
if (launch_instance) {
let godot_path: string = utils.get_configuration("editorPath", "godot");
const force_visible_collision_shapes = utils.get_configuration("forceVisibleCollisionShapes", false);
const force_visible_nav_mesh = utils.get_configuration("forceVisibleNavMesh", false);
let executable_line = `"${godot_path}" --path "${project_path}" --remote-debug ${address}:${port}`;
if (force_visible_collision_shapes) {
executable_line += " --debug-collisions";
}
if (force_visible_nav_mesh) {
executable_line += " --debug-navigation";
}
if (launch_scene) {
let filename = "";
if (scene_file) {
filename = scene_file;
} else {
filename = window.activeTextEditor.document.fileName;
}
executable_line += ` "${filename}"`;
}
if(additional_options){
executable_line += " " + additional_options;
}
executable_line += this.breakpoint_string(
debug_data.get_all_breakpoints(),
project_path
);
let godot_exec = cp.exec(executable_line, (error) => {
if (!this.terminated) {
window.showErrorMessage(`Failed to launch Godot instance: ${error}`);
}
});
this.godot_pid = godot_exec.pid;
}
this.server = net.createServer((socket) => {
this.socket = socket;
if (!launch_instance) {
let breakpoints = this.debug_data.get_all_breakpoints();
breakpoints.forEach((bp) => {
this.set_breakpoint(
this.breakpoint_path(project_path, bp.file),
bp.line
);
});
}
socket.on("data", (buffer) => {
let buffers = this.split_buffers(buffer);
while (buffers.length > 0) {
let sub_buffer = buffers.shift();
let data = this.decoder.get_dataset(sub_buffer, 0).slice(1);
this.commands.parse_message(data);
}
});
socket.on("close", (had_error) => {
Mediator.notify("stop");
});
socket.on("end", () => {
Mediator.notify("stop");
});
socket.on("error", (error) => {
Mediator.notify("error", [error]);
});
socket.on("drain", () => {
socket.resume();
this.draining = false;
this.send_buffer();
});
});
this.server.listen(port, address);
}
public step() {
this.add_and_send(this.commands.make_step_command());
}
public step_out() {
this.stepping_out = true;
this.add_and_send(this.commands.make_next_command());
}
public stop() {
this.socket?.destroy();
this.server?.close((error) => {
if (error) {
console.log(error);
}
this.server.unref();
this.server = undefined;
});
if (this.godot_pid) {
this.terminate();
}
}
private terminate() {
this.terminated = true;
TERMINATE(this.godot_pid);
this.godot_pid = undefined;
}
public trigger_breakpoint(stack_frames: GodotStackFrame[]) {
let continue_stepping = false;
let stack_count = stack_frames.length;
let file = stack_frames[0].file.replace(
"res://",
`${this.debug_data.project_path}/`
);
let line = stack_frames[0].line;
if (this.stepping_out) {
let breakpoint = this.debug_data
.get_breakpoints(file)
.find((bp) => bp.line === line);
if (!breakpoint) {
if (this.debug_data.stack_count > 1) {
continue_stepping = this.debug_data.stack_count === stack_count;
} else {
let file_same =
stack_frames[0].file === this.debug_data.last_frame.file;
let func_same =
stack_frames[0].function === this.debug_data.last_frame.function;
let line_greater =
stack_frames[0].line >= this.debug_data.last_frame.line;
continue_stepping = file_same && func_same && line_greater;
}
}
}
this.debug_data.stack_count = stack_count;
this.debug_data.last_frame = stack_frames[0];
if (continue_stepping) {
this.next();
return;
}
this.stepping_out = false;
this.debug_data.stack_files = stack_frames.map((sf) => {
return sf.file;
});
if (this.exception.length === 0) {
Mediator.notify("stopped_on_breakpoint", [stack_frames]);
} else {
Mediator.notify("stopped_on_exception", [stack_frames, this.exception]);
}
}
private add_and_send(buffer: Buffer) {
this.command_buffer.push(buffer);
this.send_buffer();
}
private breakpoint_path(project_path: string, file: string) {
let relative_path = path.relative(project_path, file).replace(/\\/g, "/");
if (relative_path.length !== 0) {
return `res://${relative_path}`;
}
return undefined;
}
private breakpoint_string(
breakpoints: GodotBreakpoint[],
project_path: string
) {
let output = "";
if (breakpoints.length > 0) {
output += " --breakpoints ";
breakpoints.forEach((bp, i) => {
output += `${this.breakpoint_path(project_path, bp.file)}:${bp.line}${i < breakpoints.length - 1 ? "," : ""
}`;
});
}
return output;
}
private send_buffer() {
if (!this.socket) {
return;
}
while (!this.draining && this.command_buffer.length > 0) {
this.draining = !this.socket.write(this.command_buffer.shift());
}
}
private split_buffers(buffer: Buffer) {
let len = buffer.byteLength;
let offset = 0;
let buffers: Buffer[] = [];
while (len > 0) {
let sub_len = buffer.readUInt32LE(offset) + 4;
buffers.push(buffer.slice(offset, offset + sub_len));
offset += sub_len;
len -= sub_len;
}
return buffers;
}
}

View File

@@ -1,24 +1,27 @@
import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
import { attemptSettingsUpdate } from "./settings_updater";
import { register_debugger } from "./debugger/debugger_context";
import { GDDocumentLinkProvider } from "./document_link_provider";
import { ClientConnectionManager } from "./lsp/ClientConnectionManager";
import { ScenePreviewProvider } from "./scene_preview_provider";
import { GodotDebugger } from "./debugger/debugger";
import { exec, execSync } from "child_process";
import {
get_configuration,
set_configuration,
find_file,
find_project_file,
register_command
register_command,
get_project_version,
set_context,
projectDir,
projectVersion,
} from "./utils";
const TOOL_NAME = "GodotTools";
import { prompt_for_godot_executable } from "./utils/prompts";
let lspClientManager: ClientConnectionManager = null;
let linkProvider: GDDocumentLinkProvider = null;
let scenePreviewManager: ScenePreviewProvider = null;
let godotDebugger: GodotDebugger = null;
export function activate(context: vscode.ExtensionContext) {
attemptSettingsUpdate(context);
@@ -26,24 +29,19 @@ export function activate(context: vscode.ExtensionContext) {
lspClientManager = new ClientConnectionManager(context);
linkProvider = new GDDocumentLinkProvider(context);
scenePreviewManager = new ScenePreviewProvider();
register_debugger(context);
godotDebugger = new GodotDebugger(context);
context.subscriptions.push(
register_command("openEditor", () => {
open_workspace_with_editor("-e").catch(err => vscode.window.showErrorMessage(err));
}),
register_command("runProject", () => {
open_workspace_with_editor().catch(err => vscode.window.showErrorMessage(err));
}),
register_command("runProjectDebug", () => {
open_workspace_with_editor("--debug-collisions --debug-navigation").catch(err => vscode.window.showErrorMessage(err));
}),
register_command("copyResourcePathContext", copy_resource_path),
register_command("openEditor", open_workspace_with_editor),
register_command("copyResourcePath", copy_resource_path),
register_command("openTypeDocumentation", open_type_documentation),
register_command("switchSceneScript", switch_scene_script),
)
);
set_context("godotFiles", ["gdscript", "gdscene", "gdresource", "gdshader",]);
set_context("sceneLikeFiles", ["gdscript", "gdscene"]);
get_project_version();
}
export function deactivate(): Thenable<void> {
@@ -89,113 +87,29 @@ async function switch_scene_script() {
}
}
function open_workspace_with_editor(params = "") {
return new Promise<void>(async (resolve, reject) => {
let valid = false;
let project_dir = '';
let project_file = '';
function open_workspace_with_editor() {
const settingName = `editorPath.godot${projectVersion[0]}`;
const godotPath = get_configuration(settingName);
if (vscode.workspace.workspaceFolders != undefined) {
const files = await vscode.workspace.findFiles("**/project.godot");
if (files) {
project_file = files[0].fsPath;
project_dir = path.dirname(project_file);
let cfg = project_file;
valid = (fs.existsSync(cfg) && fs.statSync(cfg).isFile());
}
try {
const output = execSync(`${godotPath} --version`).toString().trim();
const pattern = /([34])\.([0-9]+)\.(?:[0-9]+\.)?\w+.\w+.[0-9a-f]{9}/;
const match = output.match(pattern);
if (!match) {
const message = `Cannot launch Godot editor: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
prompt_for_godot_executable(message, settingName);
return;
}
if (valid) {
run_editor(`--path "${project_dir}" ${params}`).then(() => resolve()).catch(err => {
reject(err);
});
} else {
reject("Current workspace is not a Godot project");
if (match[1] !== settingName.slice(-1)) {
const message = `Cannot launch Godot editor: The current project uses Godot v${projectVersion}, but the specified Godot executable is version ${match[0]}`;
prompt_for_godot_executable(message, settingName);
return;
}
});
}
function run_editor(params = "") {
// TODO: rewrite this entire function
return new Promise<void>((resolve, reject) => {
const run_godot = (path: string, params: string) => {
const is_powershell_path = (path?: string) => {
const POWERSHELL = "powershell.exe";
const POWERSHELL_CORE = "pwsh.exe";
return path && (path.endsWith(POWERSHELL) || path.endsWith(POWERSHELL_CORE));
};
const escape_command = (cmd: string) => {
const cmdEsc = `"${cmd}"`;
if (process.platform === "win32") {
const shell_plugin = vscode.workspace.getConfiguration("terminal.integrated.shell");
if (shell_plugin) {
const shell = shell_plugin.get<string>("windows");
if (shell) {
if (is_powershell_path(shell)) {
return `&${cmdEsc}`;
} else {
return cmdEsc;
}
}
}
const POWERSHELL_SOURCE = "PowerShell";
const default_profile = vscode.workspace.getConfiguration("terminal.integrated.defaultProfile");
if (default_profile) {
const profile_name = default_profile.get<string>("windows");
if (profile_name) {
if (POWERSHELL_SOURCE === profile_name) {
return `&${cmdEsc}`;
}
const profiles = vscode.workspace.getConfiguration("terminal.integrated.profiles.windows");
const profile = profiles.get<{ source?: string, path?: string }>(profile_name);
if (profile) {
if (POWERSHELL_SOURCE === profile.source || is_powershell_path(profile.path)) {
return `&${cmdEsc}`;
} else {
return cmdEsc;
}
}
}
}
// default for Windows if nothing is set is PowerShell
return `&${cmdEsc}`;
}
return cmdEsc;
};
let existingTerminal = vscode.window.terminals.find(t => t.name === TOOL_NAME);
if (existingTerminal) {
existingTerminal.dispose();
}
let terminal = vscode.window.createTerminal(TOOL_NAME);
let editorPath = escape_command(path);
let cmmand = `${editorPath} ${params}`;
terminal.sendText(cmmand, true);
terminal.show();
resolve();
};
// TODO: This config doesn't exist anymore
let editorPath = get_configuration("editorPath");
if (!fs.existsSync(editorPath) || !fs.statSync(editorPath).isFile()) {
vscode.window.showOpenDialog({
openLabel: "Run",
filters: process.platform === "win32" ? { "Godot Editor Binary": ["exe", "EXE"] } : undefined
}).then((uris: vscode.Uri[]) => {
if (!uris) {
return;
}
let path = uris[0].fsPath;
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) {
reject("Invalid editor path to run the project");
} else {
run_godot(path, params);
set_configuration("editorPath", path);
}
});
} else {
run_godot(editorPath, params);
}
});
} catch {
const message = `Cannot launch Godot editor: ${settingName} of ${godotPath} is not a valid Godot executable`;
prompt_for_godot_executable(message, settingName);
return;
}
exec(`${godotPath} --path "${projectDir}" -e`);
}

View File

@@ -1,48 +1,5 @@
export class Logger {
protected buffer: string = "";
protected tag: string = "";
protected time: boolean = false;
constructor(tag: string, time: boolean) {
this.tag = tag;
this.time = time;
}
clear() {
this.buffer = "";
}
log(...messages) {
let line = "";
if (this.tag) {
line += `[${this.tag}]`;
}
if (this.time) {
line += `[${new Date().toISOString()}]`;
}
if (line) {
line += " ";
}
for (let index = 0; index < messages.length; index++) {
line += messages[index];
if (index < messages.length) {
line += " ";
} else {
line += "\n";
}
}
this.buffer += line;
console.log(line);
}
get_buffer(): string {
return this.buffer;
}
}
import { LogOutputChannel, window } from "vscode";
import { is_debug_mode } from "./utils";
export enum LOG_LEVEL {
SILENT,
@@ -58,9 +15,9 @@ const LOG_LEVEL_NAMES = [
"WARN ",
"INFO ",
"DEBUG",
]
];
const RESET = "\u001b[0m"
const RESET = "\u001b[0m";
const LOG_COLORS = [
RESET, // SILENT, normal
@@ -68,39 +25,67 @@ const LOG_COLORS = [
"\u001b[1;33m", // WARNING, yellow
"\u001b[1;36m", // INFO, cyan
"\u001b[1;32m", // DEBUG, green
]
];
export class Logger2 {
export interface LoggerOptions {
level?: LOG_LEVEL
time?: boolean;
output?: string;
}
export class Logger {
private level: LOG_LEVEL = LOG_LEVEL.DEBUG;
private show_tag: boolean = true;
private show_time: boolean;
private show_label: boolean;
private show_level: boolean = false;
private output?: LogOutputChannel;
constructor(
private tag: string,
private level: LOG_LEVEL = LOG_LEVEL.DEBUG,
{ time = false, label = false }: { time?: boolean, label?: boolean } = {},
{ level = LOG_LEVEL.DEBUG, time = false, output = "" }: LoggerOptions = {},
) {
this.level = level;
this.show_time = time;
this.show_label = label;
if (output) {
this.output = window.createOutputChannel(output, { log: true });
}
}
private log(level: LOG_LEVEL, ...messages) {
let prefix = "";
if (this.show_label) {
prefix += "[godotTools]";
}
if (this.show_time) {
prefix += `[${new Date().toISOString()}]`;
}
if (this.show_level) {
prefix += "[" + LOG_COLORS[level] + LOG_LEVEL_NAMES[level] + RESET + "]";
}
if (this.show_tag) {
prefix += "[" + LOG_COLORS[level] + this.tag + RESET + "]";
if (is_debug_mode()) {
let prefix = "";
if (this.show_time) {
prefix += `[${new Date().toISOString()}]`;
}
if (this.show_level) {
prefix += "[" + LOG_COLORS[level] + LOG_LEVEL_NAMES[level] + RESET + "]";
}
if (this.show_tag) {
prefix += "[" + LOG_COLORS[level] + this.tag + RESET + "]";
}
console.log(prefix, ...messages);
}
console.log(prefix, ...messages);
if (this.output) {
const line = `${messages[0]}`;
switch (level) {
case LOG_LEVEL.ERROR:
this.output.error(line);
break;
case LOG_LEVEL.WARNING:
this.output.warn(line);
break;
case LOG_LEVEL.INFO:
this.output.info(line);
break;
case LOG_LEVEL.DEBUG:
this.output.debug(line);
break;
default:
break;
}
}
}
info(...messages) {
@@ -125,9 +110,10 @@ export class Logger2 {
}
}
export function createLogger(tag, level: LOG_LEVEL = LOG_LEVEL.DEBUG) {
return new Logger2(tag, level);
}
const loggers: Map<string, Logger> = new Map();
const logger = new Logger("godot-tools", true);
export default logger;
export function createLogger(tag, options?: LoggerOptions) {
const logger = new Logger(tag, options);
loggers.set(tag, logger);
return logger;
}

View File

@@ -10,9 +10,10 @@ import {
register_command,
set_configuration,
} from "../utils";
import { prompt_for_godot_executable, prompt_for_reload, select_godot_executable } from "../utils/prompts";
import { createLogger } from "../logger";
import { execSync } from "child_process";
import { subProcess, killSubProcesses } from '../utils/subspawn';
import { subProcess, killSubProcesses } from "../utils/subspawn";
const log = createLogger("lsp.manager");
@@ -83,7 +84,7 @@ export class ClientConnectionManager {
}
private stop_language_server() {
killSubProcesses('LSP');
killSubProcesses("LSP");
}
private async start_language_server() {
@@ -98,11 +99,11 @@ export class ClientConnectionManager {
const projectVersion = await get_project_version();
let minimumVersion = '6';
let targetVersion = '3.6';
if (projectVersion.startsWith('4')) {
minimumVersion = '2';
targetVersion = '4.2';
let minimumVersion = "6";
let targetVersion = "3.6";
if (projectVersion.startsWith("4")) {
minimumVersion = "2";
targetVersion = "4.2";
}
const settingName = `editorPath.godot${projectVersion[0]}`;
const godotPath = get_configuration(settingName);
@@ -113,21 +114,13 @@ export class ClientConnectionManager {
const match = output.match(pattern);
if (!match) {
const message = `Cannot launch headless LSP: '${settingName}' of '${godotPath}' is not a valid Godot executable`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
}
});
prompt_for_godot_executable(message, settingName);
return;
}
this.connectedVersion = output;
if (match[1] !== projectVersion[0]) {
const message = `Cannot launch headless LSP: The current project uses Godot v${projectVersion}, but the specified Godot executable is version ${match[0]}`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
}
});
prompt_for_godot_executable(message, settingName);
return;
}
@@ -135,21 +128,17 @@ export class ClientConnectionManager {
const message = `Cannot launch headless LSP: Headless LSP mode is only available on version ${targetVersion} or newer, but the specified Godot executable is version ${match[0]}.`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Disable Headless LSP", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
select_godot_executable(settingName);
} else if (item == "Disable Headless LSP") {
set_configuration("lsp.headless", false);
this.prompt_for_reload();
prompt_for_reload();
}
});
return;
}
} catch (e) {
const message = `Cannot launch headless LSP: ${settingName} of ${godotPath} is not a valid Godot executable`;
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
if (item == "Select Godot executable") {
this.select_godot_executable(settingName);
}
});
prompt_for_godot_executable(message, settingName);
return;
}
@@ -159,10 +148,10 @@ export class ClientConnectionManager {
const headlessFlags = "--headless --no-window";
const command = `${godotPath} --path "${projectDir}" --editor ${headlessFlags} --lsp-port ${this.client.port}`;
const lspProcess = subProcess("LSP", command, { shell: true });
const lspProcess = subProcess("LSP", command, { shell: true, detached: true });
const lspStdout = createLogger("lsp.stdout");
lspProcess.stdout.on('data', (data) => {
lspProcess.stdout.on("data", (data) => {
const out = data.toString().trim();
if (out) {
lspStdout.debug(out);
@@ -170,41 +159,18 @@ export class ClientConnectionManager {
});
// const lspStderr = createLogger("lsp.stderr");
lspProcess.stderr.on('data', (data) => {
lspProcess.stderr.on("data", (data) => {
// const out = data.toString().trim();
// if (out) {
// lspStderr.debug(out);
// }
});
lspProcess.on('close', (code) => {
lspProcess.on("close", (code) => {
log.info(`LSP process exited with code ${code}`);
});
}
private async select_godot_executable(settingName: string) {
vscode.window.showOpenDialog({
openLabel: "Select Godot executable",
filters: process.platform === "win32" ? { "Godot Editor Binary": ["exe", "EXE"] } : undefined
}).then(async (uris: vscode.Uri[]) => {
if (!uris) {
return;
}
const path = uris[0].fsPath;
set_configuration(settingName, path);
this.prompt_for_reload();
});
}
private async prompt_for_reload() {
const message = `Reload VSCode to apply settings`;
vscode.window.showErrorMessage(message, "Reload").then(item => {
if (item == "Reload") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
}
private get_lsp_connection_string() {
let host = get_configuration("lsp.serverHost");
let port = get_configuration("lsp.serverPort");
@@ -250,13 +216,13 @@ export class ClientConnectionManager {
private update_status_widget() {
const lspTarget = this.get_lsp_connection_string();
const maxAttempts = get_configuration("lsp.autoReconnect.attempts")
const maxAttempts = get_configuration("lsp.autoReconnect.attempts");
let text = "";
let tooltip = "";
switch (this.status) {
case ManagerStatus.INITIALIZING:
text = `$(sync~spin) Initializing`;
tooltip = `Initializing extension...`;
text = "$(sync~spin) Initializing";
tooltip = "Initializing extension...";
break;
case ManagerStatus.INITIALIZING_LSP:
text = `$(sync~spin) Initializing LSP ${this.reconnectionAttempts}/${maxAttempts}`;
@@ -266,19 +232,19 @@ export class ClientConnectionManager {
}
break;
case ManagerStatus.PENDING:
text = `$(sync~spin) Connecting`;
text = "$(sync~spin) Connecting";
tooltip = `Connecting to the GDScript language server at ${lspTarget}`;
break;
case ManagerStatus.CONNECTED:
text = `$(check) Connected`;
text = "$(check) Connected";
tooltip = `Connected to the GDScript language server.\n${lspTarget}`;
if (this.connectedVersion) {
tooltip += `\n${this.connectedVersion}`;
}
break;
case ManagerStatus.DISCONNECTED:
text = `$(x) Disconnected`;
tooltip = `Disconnected from the GDScript language server.`;
text = "$(x) Disconnected";
tooltip = "Disconnected from the GDScript language server.";
break;
case ManagerStatus.RETRYING:
text = `$(sync~spin) Connecting ${this.reconnectionAttempts}/${maxAttempts}`;

View File

@@ -1,12 +1,12 @@
import { EventEmitter } from "events";
import * as vscode from 'vscode';
import { LanguageClient, RequestMessage, ResponseMessage, integer } from "vscode-languageclient/node";
import { createLogger } from "../logger";
import { createLogger, LOG_LEVEL } from "../logger";
import { get_configuration, set_context } from "../utils";
import { Message, MessageIO, MessageIOReader, MessageIOWriter, TCPMessageIO, WebSocketMessageIO } from "./MessageIO";
import { NativeDocumentManager } from './NativeDocumentManager';
const log = createLogger("lsp.client");
const log = createLogger("lsp.client", {level: LOG_LEVEL.SILENT});
export enum ClientStatus {
PENDING,
@@ -19,7 +19,7 @@ export enum TargetLSP {
EDITOR,
}
const CUSTOM_MESSAGE = "gdscrip_client/";
const CUSTOM_MESSAGE = "gdscript_client/";
export default class GDScriptLanguageClient extends LanguageClient {
public readonly io: MessageIO = (get_configuration("lsp.serverProtocol") == "ws") ? new WebSocketMessageIO() : new TCPMessageIO();
@@ -153,8 +153,8 @@ export default class GDScriptLanguageClient extends LanguageClient {
// this is a dirty hack to fix language server sending us prerendered
// markdown but not correctly stripping leading #'s, leading to
// docstrings being displayed as titles
const value: string = message.result["contents"].value;
message.result["contents"].value = value.replace(/\n[#]+/g, '\n');
const value: string = message.result["contents"]?.value;
message.result["contents"].value = value?.replace(/\n[#]+/g, '\n');
}
this.message_handler.on_message(message);
@@ -164,8 +164,11 @@ export default class GDScriptLanguageClient extends LanguageClient {
this.lastSymbolHovered = "";
set_context("typeFound", false);
let decl: string = message.result["contents"].value;
decl = decl.split('\n')[0].trim();
let decl: string = message?.result["contents"]?.value;
if (!decl) {
return;
}
decl = decl.split("\n")[0].trim();
// strip off the value
if (decl.includes("=")) {

View File

@@ -86,6 +86,7 @@ export class TCPMessageIO extends MessageIO {
socket.on('data', this.on_message.bind(this));
socket.on('end', this.on_disconnected.bind(this));
socket.on('close', this.on_disconnected.bind(this));
socket.on('error', this.on_error.bind(this));
});
}
@@ -98,6 +99,10 @@ export class TCPMessageIO extends MessageIO {
this.socket = null;
this.emit('disconnected');
}
protected on_error(error) {
// TODO: handle errors?
}
}
@@ -111,7 +116,7 @@ export class MessageIOReader extends AbstractMessageReader implements MessageRea
private partialMessageTimer: NodeJS.Timeout | undefined;
private _partialMessageTimeout: number;
public constructor(io: MessageIO, encoding: string = 'utf8') {
public constructor(io: MessageIO, encoding: BufferEncoding = 'utf8') {
super();
this.io = io;
this.io.reader = this;
@@ -207,7 +212,7 @@ export class MessageIOWriter extends AbstractMessageWriter implements MessageWri
private encoding: BufferEncoding;
private errorCount: number;
public constructor(io: MessageIO, encoding: string = 'utf8') {
public constructor(io: MessageIO, encoding: BufferEncoding = 'utf8') {
super();
this.io = io;
this.io.writer = this;

View File

@@ -4,7 +4,7 @@ import { EventEmitter } from "events";
import { MessageIO } from "./MessageIO";
import { NotificationMessage } from "vscode-jsonrpc";
import * as Prism from "prismjs";
import * as marked from "marked";
import { marked } from "marked";
import { get_configuration, register_command } from "../utils";
import {
Methods,
@@ -127,7 +127,7 @@ export class NativeDocumentManager extends EventEmitter {
* configuration and previously opened native symbols.
*/
private get_new_native_symbol_column(): vscode.ViewColumn {
const config_placement = get_configuration("nativeSymbolPlacement");
const config_placement = get_configuration("documentation.newTabPlacement");
if (config_placement == "active") {
return vscode.ViewColumn.Active;

View File

@@ -109,12 +109,12 @@ export class ScenePreviewProvider implements TreeDataProvider<SceneNode> {
private pin_preview() {
this.scenePreviewPinned = true;
set_context("godotTools.context.scenePreviewPinned", true);
set_context("scenePreview.pinned", true);
}
private unpin_preview() {
this.scenePreviewPinned = false;
set_context("godotTools.context.scenePreviewPinned", false);
set_context("scenePreview.pinned", false);
this.refresh();
}

View File

@@ -1,17 +1,16 @@
import * as vscode from "vscode";
const OLD_SETTINGS_CONVERSIONS = [
["godot_tools.editor_path", "godotTools.editorPath.godot3"],
["godot_tools.gdscript_lsp_server_protocol", "godotTools.lsp.serverProtocol"],
["godot_tools.gdscript_lsp_server_host", "godotTools.lsp.serverHost"],
["godot_tools.gdscript_lsp_server_port", "godotTools.lsp.serverPort"],
["godot_tools.editor_path", "godotTools.editorPath"],
["godot_tools.scene_file_config", "godotTools.sceneFileConfig"],
["godot_tools.reconnect_automatically", "godotTools.lsp.autoReconnect.enabled"],
["godot_tools.reconnect_cooldown", "godotTools.lsp.autoReconnect.cooldown"],
["godot_tools.reconnect_attempts", "godotTools.lsp.autoReconnect.attempts"],
["godot_tools.force_visible_collision_shapes", "godotTools.forceVisibleCollisionShapes"],
["godot_tools.force_visible_nav_mesh", "godotTools.forceVisibleNavMesh"],
["godot_tools.native_symbol_placement", "godotTools.nativeSymbolPlacement"],
["godot_tools.force_visible_collision_shapes", "godotTools.debugger.forceVisibleCollisionShapes"],
["godot_tools.force_visible_nav_mesh", "godotTools.debugger.forceVisibleNavMesh"],
["godot_tools.native_symbol_placement", "godotTools.documentation.newTabPlacement"],
["godot_tools.scenePreview.previewRelatedScenes", "godotTools.scenePreview.previewRelatedScenes"]
];
@@ -23,7 +22,6 @@ export function updateOldStyleSettings() {
if (value === undefined) {
continue;
}
configuration.update(old_style_key, undefined, true);
configuration.update(new_style_key, value, true);
settings_changed = true;
}

View File

@@ -5,12 +5,12 @@ import { AddressInfo, createServer } from "net";
const EXTENSION_PREFIX = "godotTools";
export function get_configuration(name: string, default_value?: any) {
let config_value = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(name, null);
if (default_value && config_value === null) {
return default_value;
export function get_configuration(name: string, defaultValue?: any) {
const configValue = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(name, null);
if (defaultValue && configValue === null) {
return defaultValue;
}
return config_value;
return configValue;
}
export function set_configuration(name: string, value: any) {
@@ -40,16 +40,19 @@ export function get_word_under_cursor(): string {
return symbolName;
}
export async function get_project_version(): Promise<string | undefined> {
const project_dir = await get_project_dir();
export let projectVersion = undefined;
if (!project_dir) {
export async function get_project_version(): Promise<string | undefined> {
const dir = await get_project_dir();
if (!dir) {
projectVersion = undefined;
return undefined;
}
let godot_version = '3.x';
const project_file = vscode.Uri.file(path.join(project_dir, 'project.godot'));
const document = await vscode.workspace.openTextDocument(project_file);
let godotVersion = "3.x";
const projectFile = vscode.Uri.file(path.join(dir, "project.godot"));
const document = await vscode.workspace.openTextDocument(projectFile);
const text = document.getText();
const match = text.match(/config\/features=PackedStringArray\((.*)\)/);
@@ -57,25 +60,30 @@ export async function get_project_version(): Promise<string | undefined> {
const line = match[0];
const version = line.match(/\"(4.[0-9]+)\"/);
if (version) {
godot_version = version[1];
godotVersion = version[1];
}
}
return godot_version;
projectVersion = godotVersion;
return godotVersion;
}
export let projectDir = undefined;
export async function get_project_dir() {
let project_dir = undefined;
let project_file = '';
let dir = undefined;
let projectFile = "";
if (vscode.workspace.workspaceFolders != undefined) {
const files = await vscode.workspace.findFiles("**/project.godot");
if (files) {
project_file = files[0].fsPath;
if (fs.existsSync(project_file) && fs.statSync(project_file).isFile()) {
project_dir = path.dirname(project_file);
projectFile = files[0].fsPath;
if (fs.existsSync(projectFile) && fs.statSync(projectFile).isFile()) {
dir = path.dirname(projectFile);
}
}
}
return project_dir;
projectDir = dir;
return dir;
}
export function find_project_file(start: string, depth: number = 20) {
@@ -86,10 +94,10 @@ export function find_project_file(start: string, depth: number = 20) {
if (start == folder) {
return null;
}
const project_file = path.join(folder, "project.godot");
const projectFile = path.join(folder, "project.godot");
if (fs.existsSync(project_file) && fs.statSync(project_file).isFile()) {
return project_file;
if (fs.existsSync(projectFile) && fs.statSync(projectFile).isFile()) {
return projectFile;
} else {
if (depth === 0) {
return null;
@@ -116,8 +124,8 @@ export async function convert_resource_path_to_uri(resPath: string): Promise<vsc
if (!files) {
return null;
}
const project_dir = files[0].fsPath.replace("project.godot", "");
return vscode.Uri.joinPath(vscode.Uri.file(project_dir), resPath.substring(6));
const dir = files[0].fsPath.replace("project.godot", "");
return vscode.Uri.joinPath(vscode.Uri.file(dir), resPath.substring(6));
}
export async function get_free_port(): Promise<number> {

33
src/utils/prompts.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as vscode from "vscode";
import { set_configuration } from "../utils";
export function prompt_for_reload() {
const message = "Reload VSCode to apply settings";
vscode.window.showErrorMessage(message, "Reload").then(item => {
if (item == "Reload") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
}
export function select_godot_executable(settingName: string) {
vscode.window.showOpenDialog({
openLabel: "Select Godot executable",
filters: process.platform === "win32" ? { "Godot Editor Binary": ["exe", "EXE"] } : undefined
}).then(async (uris: vscode.Uri[]) => {
if (!uris) {
return;
}
const path = uris[0].fsPath;
set_configuration(settingName, path);
prompt_for_reload();
});
}
export function prompt_for_godot_executable(message: string, settingName: string) {
vscode.window.showErrorMessage(message, "Select Godot executable", "Ignore").then(item => {
if (item == "Select Godot executable") {
select_godot_executable(settingName);
}
});
}

View File

@@ -5,7 +5,10 @@ Original library copyright (c) 2022 Craig Wardman
I had to vendor this library to fix the API in a couple places.
*/
import { ChildProcess, execSync, spawn, SpawnOptions } from 'child_process';
import { ChildProcess, execSync, spawn, SpawnOptions } from "child_process";
import { createLogger } from "../logger";
const log = createLogger("subspawn");
interface DictionaryOfStringChildProcessArray {
[key: string]: ChildProcess[];
@@ -20,17 +23,23 @@ export function killSubProcesses(owner: string) {
children[owner].forEach((c) => {
try {
if (c.pid) {
if (process.platform === 'win32') {
if (process.platform === "win32") {
execSync(`taskkill /pid ${c.pid} /T /F`);
} else if (process.platform === "darwin") {
execSync(`kill -9 ${c.pid}`);
} else {
process.kill(-c.pid);
process.kill(c.pid);
}
}
} catch { }
} catch {
log.error(`couldn't kill task ${owner}`);
}
});
children[owner] = [];
}
process.on('exit', () => {
process.on("exit", () => {
Object.keys(children).forEach((owner) => killSubProcesses(owner));
});
@@ -38,9 +47,9 @@ function gracefulExitHandler() {
process.exit();
}
process.on('SIGINT', gracefulExitHandler);
process.on('SIGTERM', gracefulExitHandler);
process.on('SIGQUIT', gracefulExitHandler);
process.on("SIGINT", gracefulExitHandler);
process.on("SIGTERM", gracefulExitHandler);
process.on("SIGQUIT", gracefulExitHandler);
export function subProcess(owner: string, command: string, options?: SpawnOptions) {
const childProcess = spawn(command, options);