diff --git a/README.md b/README.md index b2a7d39..bd3aa7b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ experience as comfortable as possible: - Ctrl + click on a variable or method call to jump to its definition - Full documentation of the Godot Engine's API supported - Run a Godot project from VS Code -- Debug your Godot project from VS Code with breakpoints, step-in, and call stack +- Debug your Godot project from VS Code with breakpoints, step-in/out/over, variable watch, call stack, and active scene tree ![Showing the documentation on hover feature](img/godot-tools.png) @@ -33,16 +33,6 @@ The extension adds a few entries to the VS Code Command Palette under "Godot Too - Run the workspace as a Godot project - List Godot's native classes -## Debugger - -To configure the debugger: - -1. Open the command palette: -2. `>Debug: Open launch.json` -3. Select the Debug Godot configuration. -4. Change any relevant settings. -5. Press F5 to launch. - ## Settings ### Godot @@ -64,6 +54,37 @@ You can use the following settings to configure Godot Tools: - `gdscript_lsp_server_port` - The WebSocket server port of the GDScript language server. - `check_status` - Check the GDScript language server connection status. +#### Debugger + +To configure the debugger: + +1. Open the command palette: +2. `>Debug: Open launch.json` +3. Select the Debug Godot configuration. +4. Change any relevant settings. +5. Press F5 to launch. + +*Configurations* + +_Required_ + +- "project": Absolute path to a directory with a project.godot file. Defaults to the currently open VSCode workspace with `${workspaceFolder}`. +- "port": Number that represents the port the Godot remote debugger will connect with. Defaults to `6007`. +- "address": String that represents the IP address that the Godot remote debugger will connect to. Defaults to `127.0.0.1`. + +_Optional_ + +- "launch_game_instance": true/false. If true, an instance of Godot will be launched. Will use the path provided in `editor_path`. Defaults to `true`. +- "launch_scene": true/false. If true, and launch_game_instance is true, will launch an instance of Godot to a currently active opened TSCN file. Defaults to `false`. +- "scene_file": Path _relative to the project.godot file_ to a TSCN file. If launch_game_instance and launch_scene are both true, will use this file instead of looking for the currently active opened TSCN file. + +*Usage* + +- Stacktrace and variable dumps are the same as any regular debugger +- The active scene tree can be refreshed with the Refresh icon in the top right. +- Nodes can be brought to the fore in the Inspector by clicking the Eye icon next to nodes in the active scene tree, or Objects in the inspector. +- You can edit integers, floats, strings, and booleans within the inspector by clicking the pencil icon next to each. + ## Issues and contributions The [Godot Tools](https://github.com/godotengine/godot-vscode-plugin) extension diff --git a/package.json b/package.json index 4a3dc19..e837196 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,8 @@ "tscn", "godot", "gdns", - "gdnlib" + "gdnlib", + "import" ] } ], @@ -138,7 +139,7 @@ { "type": "godot", "label": "Godot Debug", - "program": "./out/debugger/debugAdapter.js", + "program": "./out/debugger/debug_adapter.js", "runtime": "node", "configurationAttributes": { "launch": { @@ -167,6 +168,16 @@ "type": "boolean", "description": "Whether to launch an instance of the workspace's game, or wait for a debug session to connect.", "default": true + }, + "launch_scene": { + "type": "boolean", + "description": "Whether to launch an instance the currently opened TSCN file, or launch the game project. Only works with launch_game_instance being true.", + "default": false + }, + "scene_file": { + "type": "string", + "description": "Relative path from the godot.project file to a TSCN file. If launch_scene and launch_game_instance are true, and this file is defined, will launch the specified file instead of looking for an active TSCN file.", + "default": "" } } } @@ -179,7 +190,8 @@ "project": "${workspaceFolder}", "port": 6007, "address": "127.0.0.1", - "launch_game_instance": true + "launch_game_instance": true, + "launch_scene": false } ], "configurationSnippets": [ @@ -192,7 +204,8 @@ "project": "${workspaceFolder}", "port": 6007, "address": "127.0.0.1", - "launch_game_instance": true + "launch_game_instance": true, + "launch_scene": false } } ] diff --git a/src/debugger/SceneTree/inspector_provider.ts b/src/debugger/SceneTree/inspector_provider.ts deleted file mode 100644 index 9a0fba0..0000000 --- a/src/debugger/SceneTree/inspector_provider.ts +++ /dev/null @@ -1,433 +0,0 @@ -import { - TreeDataProvider, - EventEmitter, - Event, - ProviderResult, - TreeItem, - TreeItemCollapsibleState -} from "vscode"; -import { RemotePropertyBuilder } from "./tree_builders"; - -export class InspectorProvider implements TreeDataProvider { - private _on_did_change_tree_data: EventEmitter< - RemoteProperty | undefined - > = new EventEmitter(); - private tree: RemoteProperty | undefined; - - public readonly onDidChangeTreeData: Event | undefined = this - ._on_did_change_tree_data.event; - - constructor() {} - - public clean_up() { - if (this.tree) { - this.tree = undefined; - this._on_did_change_tree_data.fire(); - } - } - - public fill_tree( - element_name: string, - class_name: string, - object_id: number, - properties: any[] - ) { - this.tree = RemotePropertyBuilder.build( - element_name, - class_name, - object_id, - properties - ); - - this.tree.description = class_name; - this._on_did_change_tree_data.fire(); - } - - public getChildren( - element?: RemoteProperty - ): ProviderResult { - if (!this.tree) { - return Promise.resolve([]); - } - - if (!element) { - return Promise.resolve([this.tree]); - } else { - return Promise.resolve(element.properties); - } - } - - public getTreeItem(element: RemoteProperty): TreeItem | Thenable { - return element; - } - - public get_changed_value( - parents: RemoteProperty[], - property: RemoteProperty, - new_parsed_value: any - ) { - let idx = parents.length - 1; - let value = parents[idx].value; - switch (value.__type__) { - case "Vector2": - { - let name = property.label; - switch (name) { - case "x": - value.x = new_parsed_value; - break; - case "y": - value.y = new_parsed_value; - break; - } - } - break; - case "Rect2": - { - let name = property.label; - let vector = parents[idx - 1].label; - switch (vector) { - case "position": - switch (name) { - case "x": - value.position.x = new_parsed_value; - break; - case "y": - value.position.y = new_parsed_value; - break; - } - break; - case "size": - switch (name) { - case "x": - value.size.x = new_parsed_value; - break; - case "y": - value.size.y = new_parsed_value; - break; - } - break; - } - } - break; - case "Vector3": - { - let name = property.label; - switch (name) { - case "x": - value.x = new_parsed_value; - break; - case "y": - value.y = new_parsed_value; - break; - case "z": - value.z = new_parsed_value; - break; - } - } - break; - case "Transform2D": - { - let name = property.label; - let vector = parents[idx - 1].label; - switch (vector) { - case "origin": - switch (name) { - case "x": - value.position.x = new_parsed_value; - break; - case "y": - value.position.y = new_parsed_value; - break; - } - break; - case "x": - switch (name) { - case "x": - value.size.x = new_parsed_value; - break; - case "y": - value.size.y = new_parsed_value; - break; - } - break; - case "y": - switch (name) { - case "x": - value.size.x = new_parsed_value; - break; - case "y": - value.size.y = new_parsed_value; - break; - } - break; - } - } - break; - case "Plane": - { - let name = property.label; - let subprop = parents[idx - 1].label; - switch (subprop) { - case "d": - value.d = new_parsed_value; - break; - case "x": - value.x = new_parsed_value; - break; - case "y": - value.y = new_parsed_value; - break; - case "z": - value.z = new_parsed_value; - break; - case "normal": - switch (name) { - case "x": - value.normal.x = new_parsed_value; - break; - case "y": - value.normal.y = new_parsed_value; - break; - case "z": - value.normal.z = new_parsed_value; - break; - } - break; - } - } - break; - case "Quat": - { - let name = property.label; - switch (name) { - case "x": - value.x = new_parsed_value; - break; - case "y": - value.y = new_parsed_value; - break; - case "z": - value.z = new_parsed_value; - break; - case "w": - value.w = new_parsed_value; - break; - } - } - break; - case "AABB": - { - let name = property.label; - let vector = parents[idx - 1].label; - switch (vector) { - case "end": - switch (name) { - case "x": - value.end.x = new_parsed_value; - break; - case "y": - value.end.y = new_parsed_value; - break; - case "z": - value.end.z = new_parsed_value; - break; - } - break; - case "position": - switch (name) { - case "x": - value.position.x = new_parsed_value; - break; - case "y": - value.position.y = new_parsed_value; - break; - case "z": - value.position.z = new_parsed_value; - break; - } - break; - case "size": - switch (name) { - case "x": - value.size.x = new_parsed_value; - break; - case "y": - value.size.y = new_parsed_value; - break; - case "z": - value.size.z = new_parsed_value; - break; - } - break; - } - } - break; - case "Basis": - { - let name = property.label; - let vector = parents[idx - 1].label; - switch (vector) { - case "x": - switch (name) { - case "x": - value.x.x = new_parsed_value; - break; - case "y": - value.x.y = new_parsed_value; - break; - case "z": - value.x.z = new_parsed_value; - break; - } - break; - case "y": - switch (name) { - case "x": - value.y.x = new_parsed_value; - break; - case "y": - value.y.y = new_parsed_value; - break; - case "z": - value.y.z = new_parsed_value; - break; - } - break; - case "z": - switch (name) { - case "x": - value.z.x = new_parsed_value; - break; - case "y": - value.z.y = new_parsed_value; - break; - case "z": - value.z.z = new_parsed_value; - break; - } - break; - } - } - break; - case "Transform": - { - let name = property.label; - let parent_name = parents[idx - 1].label; - if ( - parent_name === "x" || - parent_name === "y" || - parent_name === "z" - ) { - switch (name) { - case "x": - switch (parent_name) { - case "x": - value.basis.x.x = new_parsed_value; - break; - case "y": - value.basis.x.y = new_parsed_value; - break; - case "z": - value.basis.x.z = new_parsed_value; - break; - } - break; - case "y": - switch (parent_name) { - case "x": - value.basis.y.x = new_parsed_value; - break; - case "y": - value.basis.y.y = new_parsed_value; - break; - case "z": - value.basis.y.z = new_parsed_value; - break; - } - break; - case "z": - switch (parent_name) { - case "x": - value.basis.z.x = new_parsed_value; - break; - case "y": - value.basis.z.y = new_parsed_value; - break; - case "z": - value.basis.z.z = new_parsed_value; - break; - } - break; - } - } else { - switch (name) { - case "x": - value.origin.x = new_parsed_value; - break; - case "y": - value.origin.y = new_parsed_value; - break; - case "z": - value.origin.z = new_parsed_value; - break; - } - } - } - break; - case "Color": - { - let name = property.label; - switch (name) { - case "r": - value.r = new_parsed_value; - break; - case "g": - value.g = new_parsed_value; - break; - case "b": - value.b = new_parsed_value; - break; - case "a": - value.a = new_parsed_value; - break; - } - } - break; - default: - if (Array.isArray(value)) { - let idx = parseInt(property.label); - if (idx < value.length) { - value[idx] = new_parsed_value; - } - } - else if(value instanceof Map) { - value.set(property.parent.value.key, new_parsed_value); - } - break; - } - - return value; - } - - public has_tree() { - return this.tree !== undefined; - } -} - -export class RemoteProperty extends TreeItem { - public changes_parent?: boolean; - public parent?: RemoteProperty; - - constructor( - public label: string, - public value: any, - public object_id: number, - public properties: RemoteProperty[], - public collapsibleState?: TreeItemCollapsibleState - ) { - super(label, collapsibleState); - } -} - -export class RemoteObject extends RemoteProperty {} diff --git a/src/debugger/SceneTree/tree_builders.ts b/src/debugger/SceneTree/tree_builders.ts deleted file mode 100644 index b322a03..0000000 --- a/src/debugger/SceneTree/tree_builders.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { SceneNode } from "./scene_tree_provider"; -import { RemoteProperty, RemoteObject } from "./inspector_provider"; -import stringify from "../stringify"; -import { TreeItemCollapsibleState } from "vscode"; - -export class SceneTreeBuilder { - public static build(params: any[]) { - return this.parse_next(params, { offset: 0 }); - } - - private static 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); - } -} - -export class RemotePropertyBuilder { - private static build_property(object_id: number, property: any[], is_dict_key?: boolean) { - let prop_name: string = property[0]; - let prop_value: any = property[5]; - let is_remote_object = false; - let is_primitive = false; - - let child_props: RemoteProperty[] = []; - if (Array.isArray(prop_value) || prop_value instanceof Map) { - let length = 0; - let values: any[]; - if (prop_value instanceof Map) { - length = prop_value.size; - let keys = Array.from(prop_value.keys()); - values = keys.map(key => { - let value = prop_value.get(key); - let stringified_key = stringify(key).value; - - return { - __type__: "Pair", - key: key, - value: value, - __render__: () => stringified_key - }; - }); - } else { - length = prop_value.length; - values = prop_value; - } - for (let i = 0; i < length; i++) { - let name = `${i}`; - let child_prop = this.build_property(object_id, [ - name, - 0, - 0, - 0, - 0, - values[i] - ]); - child_prop.changes_parent = true; - child_props.push(child_prop); - } - } else if (typeof prop_value === "object") { - if (prop_value.__type__ && prop_value.__type__ === "Object") { - is_remote_object = true; - } else { - for (const PROP in prop_value) { - if (PROP !== "__type__" && PROP !== "__render__") { - let name = `${PROP}`; - let child_prop = this.build_property(object_id, [ - name, - 0, - 0, - 0, - 0, - prop_value[PROP] - ], prop_value.__type__ === "Pair" && name === "key"); - child_prop.changes_parent = true; - child_props.push(child_prop); - } - } - } - } else if (!is_dict_key) { - is_primitive = true; - } - - let out_prop = new RemoteProperty( - prop_name, - prop_value, - object_id, - child_props, - child_props.length === 0 - ? TreeItemCollapsibleState.None - : TreeItemCollapsibleState.Collapsed - ); - out_prop.properties.forEach(prop => { - prop.parent = out_prop; - }); - out_prop.description = stringify(prop_value).value; - if (is_remote_object) { - out_prop.contextValue = "remote_object"; - } else if (is_primitive) { - out_prop.contextValue = "editable_value"; - } - return out_prop; - } - - public static build( - element_name: string, - class_name: string, - object_id: number, - properties: any[][] - ) { - let categories = [ - ["Node", 0, 0, 0, 0, undefined], - ...properties.filter(value => value[5] === undefined) - ]; - - let categorized_props: RemoteProperty[] = []; - for (let i = 0; i < categories.length - 1; i++) { - const category = categories[i]; - - let props = - i > 0 - ? properties.slice( - properties.findIndex(value => category === value) + 1, - properties.findIndex(value => categories[i + 1] === value) - ) - : properties.slice( - 0, - properties.findIndex(value => categories[1] === value) - ); - - let out_props = props.map(value => { - return this.build_property(object_id, value); - }); - - let category_prop = new RemoteProperty( - category[0], - undefined, - object_id, - out_props, - TreeItemCollapsibleState.Expanded - ); - - categorized_props.push(category_prop); - } - - let out = new RemoteProperty( - element_name, - undefined, - object_id, - categorized_props, - TreeItemCollapsibleState.Expanded - ); - out.description = class_name; - return out; - } -} diff --git a/src/debugger/commands/command.ts b/src/debugger/commands/command.ts new file mode 100644 index 0000000..4f32452 --- /dev/null +++ b/src/debugger/commands/command.ts @@ -0,0 +1,7 @@ +import { Mediator } from "../mediator"; + +export abstract class Command { + public param_count: number = -1; + + public abstract trigger(parameters: any[]): void; +} diff --git a/src/debugger/commands/command_parser.ts b/src/debugger/commands/command_parser.ts new file mode 100644 index 0000000..fecd789 --- /dev/null +++ b/src/debugger/commands/command_parser.ts @@ -0,0 +1,162 @@ +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 Command> = new Map([ + [ + "output", + function () { + return new CommandOutput(); + }, + ], + [ + "message:scene_tree", + function () { + return new CommandMessageSceneTree(); + }, + ], + [ + "message:inspect_object", + function () { + return new CommandMessageInspectObject(); + }, + ], + [ + "message:scene_tree", + function () { + return new CommandMessageSceneTree(); + }, + ], + [ + "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) { + this.current_command.trigger(this.parameters); + this.current_command = undefined; + this.parameters = []; + } + } 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; + } +} diff --git a/src/debugger/commands/commands/command_debug_enter.ts b/src/debugger/commands/commands/command_debug_enter.ts new file mode 100644 index 0000000..7247fd9 --- /dev/null +++ b/src/debugger/commands/commands/command_debug_enter.ts @@ -0,0 +1,9 @@ +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]); + } +} diff --git a/src/debugger/commands/commands/command_debug_exit.ts b/src/debugger/commands/commands/command_debug_exit.ts new file mode 100644 index 0000000..c6400b5 --- /dev/null +++ b/src/debugger/commands/commands/command_debug_exit.ts @@ -0,0 +1,8 @@ +import { Command } from "../command"; +import { Mediator } from "../../mediator"; + +export class CommandDebugExit extends Command { + public trigger(parameters: any[]) { + Mediator.notify("debug_exit"); + } +} diff --git a/src/debugger/commands/commands/command_message_inspect_object.ts b/src/debugger/commands/commands/command_message_inspect_object.ts new file mode 100644 index 0000000..42a1311 --- /dev/null +++ b/src/debugger/commands/commands/command_message_inspect_object.ts @@ -0,0 +1,18 @@ +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]); + } +} diff --git a/src/debugger/commands/commands/command_message_scene_tree.ts b/src/debugger/commands/commands/command_message_scene_tree.ts new file mode 100644 index 0000000..8bd1b47 --- /dev/null +++ b/src/debugger/commands/commands/command_message_scene_tree.ts @@ -0,0 +1,25 @@ +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); + } +} diff --git a/src/debugger/commands/commands/command_null.ts b/src/debugger/commands/commands/command_null.ts new file mode 100644 index 0000000..c4ea53b --- /dev/null +++ b/src/debugger/commands/commands/command_null.ts @@ -0,0 +1,5 @@ +import { Command } from "../command"; + +export class CommandNull extends Command { + public trigger(parameters: any[]) {} +} diff --git a/src/debugger/commands/commands/command_output.ts b/src/debugger/commands/commands/command_output.ts new file mode 100644 index 0000000..afcab3b --- /dev/null +++ b/src/debugger/commands/commands/command_output.ts @@ -0,0 +1,9 @@ +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); + } +} diff --git a/src/debugger/commands/commands/command_stack_dump.ts b/src/debugger/commands/commands/command_stack_dump.ts new file mode 100644 index 0000000..e88fc2a --- /dev/null +++ b/src/debugger/commands/commands/command_stack_dump.ts @@ -0,0 +1,17 @@ +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); + } +} diff --git a/src/debugger/commands/commands/command_stack_frame_vars.ts b/src/debugger/commands/commands/command_stack_frame_vars.ts new file mode 100644 index 0000000..5801175 --- /dev/null +++ b/src/debugger/commands/commands/command_stack_frame_vars.ts @@ -0,0 +1,31 @@ +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]); + } +} diff --git a/src/debugger/communication/command.ts b/src/debugger/communication/command.ts deleted file mode 100644 index 112d9a7..0000000 --- a/src/debugger/communication/command.ts +++ /dev/null @@ -1,60 +0,0 @@ -export class Command { - private callback?: ( - parameters: Array - ) => void | undefined; - private param_count = -1; - private param_count_callback?: (paramCount: number) => number; - private parameters: Array< - boolean | number | string | {} | [] | undefined - > = []; - - public name: string; - - constructor( - name: string, - parameters_fulfilled?: (parameters: Array) => void | undefined, - modify_param_count?: (param_count: number) => number - ) { - this.name = name; - this.callback = parameters_fulfilled; - this.param_count_callback = modify_param_count; - } - - public append_parameters( - parameter: boolean | number | string | {} | [] | undefined - ) { - if (this.param_count <= 0) { - this.param_count = parameter as number; - if (this.param_count === 0) { - if (this.callback) { - this.callback([]); - } - } - return; - } - - this.parameters.push(parameter); - - if (this.parameters.length === this.get_param_count()) { - if (this.callback) { - this.callback(this.parameters); - } - } - } - - public chain() { - if (this.parameters.length === this.get_param_count()) { - this.parameters.length = 0; - this.param_count = -1; - return undefined; - } else { - return this; - } - } - - protected get_param_count() { - return this.param_count_callback - ? this.param_count_callback(this.param_count) - : this.param_count; - } -} diff --git a/src/debugger/communication/command_builder.ts b/src/debugger/communication/command_builder.ts deleted file mode 100644 index 980636d..0000000 --- a/src/debugger/communication/command_builder.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Command } from "./command"; -import { VariantParser } from "../variant_parser"; - -export class CommandBuilder { - private commands = new Map(); - private current_command?: Command; - private dummy_command = new Command("---"); - - constructor() {} - - public create_buffered_command( - command: string, - parser: VariantParser, - parameters?: any[] - ): Buffer { - let command_array: any[] = [command]; - if (parameters) { - parameters?.forEach(param => { - command_array.push(param); - }); - } - - let buffer = parser.encode_variant(command_array); - return buffer; - } - - public parse_data( - dataset: Array, - error_callback: (error: string) => void - ): void { - while (dataset && dataset.length > 0) { - if (this.current_command) { - let next_command = this.current_command.chain(); - if (next_command === this.current_command) { - this.current_command.append_parameters(dataset.shift()); - } else { - this.current_command = next_command; - } - } else { - let data = dataset.shift(); - if (data) { - let command = this.commands.get(data); - if (command) { - this.current_command = command; - } else { - error_callback(`Unsupported command: ${data}. Skipping.`); - this.current_command = this.dummy_command; - } - } - } - } - } - - public register_command(command: Command) { - let name = command.name; - this.commands.set(name, command); - } -} diff --git a/src/debugger/communication/godot_commands.ts b/src/debugger/communication/godot_commands.ts deleted file mode 100644 index 3951837..0000000 --- a/src/debugger/communication/godot_commands.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { CommandBuilder } from "./command_builder"; -import { VariantParser } from "../variant_parser"; -import net = require("net"); - -export class GodotCommands { - private builder: CommandBuilder; - private can_write = false; - private command_buffer: Buffer[] = []; - private connection: net.Socket | undefined; - private parser: VariantParser; - - constructor( - builder: CommandBuilder, - parser: VariantParser, - connection?: net.Socket - ) { - this.builder = builder; - this.parser = parser; - this.connection = connection; - } - - public send_break_command() { - let buffer = this.builder.create_buffered_command("break", this.parser); - this.add_and_send(buffer); - } - - public send_continue_Command() { - let buffer = this.builder.create_buffered_command("continue", this.parser); - this.add_and_send(buffer); - } - - public send_inspect_object_command(object_id: number) { - let buffer = this.builder.create_buffered_command( - "inspect_object", - this.parser, - [object_id] - ); - - this.add_and_send(buffer); - } - - public set_object_property( - object_id: number, - label: string, - new_parsed_value: any - ) { - let buffer = this.builder.create_buffered_command( - "set_object_property", - this.parser, - [BigInt(object_id), label, new_parsed_value] - ); - this.add_and_send(buffer); - } - - public send_next_command() { - let buffer = this.builder.create_buffered_command("next", this.parser); - this.add_and_send(buffer); - } - - public send_remove_breakpoint_command(file: string, line: number) { - this.send_breakpoint_command(false, file, line); - } - - public send_request_scene_tree_command() { - let buffer = this.builder.create_buffered_command( - "request_scene_tree", - this.parser - ); - this.add_and_send(buffer); - } - - public send_set_breakpoint_command(file: string, line: number) { - this.send_breakpoint_command(true, file, line); - } - - public send_skip_breakpoints_command(skip_breakpoints: boolean) { - let buffer = this.builder.create_buffered_command( - "set_skip_breakpoints", - this.parser, - [skip_breakpoints] - ); - - this.add_and_send(buffer); - } - - public send_stack_dump_command() { - let buffer = this.builder.create_buffered_command( - "get_stack_dump", - this.parser - ); - - this.add_and_send(buffer); - } - - public send_stack_frame_vars_command(level: number) { - let buffer = this.builder.create_buffered_command( - "get_stack_frame_vars", - this.parser, - [level] - ); - - this.add_and_send(buffer); - } - - public send_step_command() { - let buffer = this.builder.create_buffered_command("step", this.parser); - this.add_and_send(buffer); - } - - public set_can_write(value: boolean) { - this.can_write = value; - if (this.can_write) { - this.send_buffer(); - } - } - - public set_connection(connection: net.Socket) { - this.connection = connection; - this.can_write = true; - } - - private add_and_send(buffer: Buffer) { - this.command_buffer.push(buffer); - this.send_buffer(); - } - - private send_breakpoint_command(set: boolean, file: string, line: number) { - let buffer = this.builder.create_buffered_command( - "breakpoint", - this.parser, - [file, line, set] - ); - this.add_and_send(buffer); - } - - private send_buffer() { - if (!this.connection) { - return; - } - - while (this.can_write && this.command_buffer.length > 0) { - this.can_write = this.connection.write( - this.command_buffer.shift() as Buffer - ); - } - } -} diff --git a/src/debugger/communication/server_controller.ts b/src/debugger/communication/server_controller.ts deleted file mode 100644 index 20272a7..0000000 --- a/src/debugger/communication/server_controller.ts +++ /dev/null @@ -1,454 +0,0 @@ -const TERMINATE = require("terminate"); -import { EventEmitter } from "events"; -import net = require("net"); -import cp = require("child_process"); -import path = require("path"); -import { VariantParser } from "../variant_parser"; -import { Command } from "./command"; -import vscode = require("vscode"); -import { GodotCommands } from "./godot_commands"; -import { CommandBuilder } from "./command_builder"; -import { GodotBreakpoint, GodotStackFrame } from "../godot_debug_runtime"; -import utils = require("../../utils"); -import { SceneTreeBuilder } from "../SceneTree/tree_builders"; -import { SceneTreeProvider } from "../SceneTree/scene_tree_provider"; - -export class ServerController { - private breakpoints: { file: string; line: number }[] = []; - private builder: CommandBuilder | undefined; - private connection: net.Socket | undefined; - private emitter: EventEmitter; - private exception = ""; - private godot_commands: GodotCommands | undefined; - private godot_pid: number | undefined; - private initial_output = false; - private inspected_callbacks: (( - class_name: string, - properties: any[] - ) => void)[] = []; - private last_frame: - | { line: number; file: string; function: string } - | undefined; - private log_output = ""; - private logging = false; - private output_channel: vscode.OutputChannel | undefined; - private parser: VariantParser | undefined; - private project_path: string; - private scope_callbacks: (( - stack_level: number, - stack_files: string[], - scopes: { - locals: { name: string; value: any }[]; - members: { name: string; value: any }[]; - globals: { name: string; value: any }[]; - } - ) => void)[] = []; - private server: net.Server | undefined; - private stack_count = 0; - private stack_files: string[] = []; - private stack_level = 0; - private stepping_out = false; - private tree_provider: SceneTreeProvider | undefined; - - constructor( - event_emitter: EventEmitter, - output_channel?: vscode.OutputChannel, - tree_provider?: SceneTreeProvider - ) { - this.emitter = event_emitter; - this.output_channel = output_channel; - this.tree_provider = tree_provider; - } - - public break() { - this.godot_commands?.send_break_command(); - } - - public continue() { - this.godot_commands?.send_continue_Command(); - } - - public get_scope( - level: number, - callback?: ( - stack_level: number, - stack_files: string[], - scopes: { - locals: { name: string; value: any }[]; - members: { name: string; value: any }[]; - globals: { name: string; value: any }[]; - } - ) => void - ) { - this.godot_commands?.send_stack_frame_vars_command(level); - this.stack_level = level; - if (callback) { - this.scope_callbacks.push(callback); - } - } - - public inspect_object( - id: number, - inspected: (class_name: string, properties: any[]) => void - ) { - this.inspected_callbacks.push(inspected); - this.godot_commands?.send_inspect_object_command(id); - } - - public next() { - this.godot_commands?.send_next_command(); - } - - public remove_breakpoint(path_to: string, line: number) { - this.breakpoints.splice( - this.breakpoints.findIndex(bp => bp.file === path_to && bp.line === line), - 1 - ); - this.godot_commands?.send_remove_breakpoint_command(path_to, line); - } - - public request_scene_tree() { - this.godot_commands.send_request_scene_tree_command(); - } - - public set_object_property( - object_id: number, - label: string, - new_parsed_value: any - ) { - this.godot_commands.set_object_property(object_id, label, new_parsed_value); - } - - public set_breakpoint(path_to: string, line: number) { - this.breakpoints.push({ file: path_to, line: line }); - this.godot_commands?.send_set_breakpoint_command(path_to, line); - } - - public start( - project_path: string, - port: number, - address: string, - launch_game_instance?: boolean, - breakpoints?: GodotBreakpoint[] - ) { - this.builder = new CommandBuilder(); - this.parser = new VariantParser(); - this.project_path = project_path.replace(/\\/g, "/"); - if (this.project_path.match(/^[A-Z]:\//)) { - this.project_path = - this.project_path[0].toLowerCase() + this.project_path.slice(1); - } - this.godot_commands = new GodotCommands(this.builder, this.parser); - - if (breakpoints) { - this.breakpoints = breakpoints.map(bp => { - return { file: bp.file, line: bp.line }; - }); - } - - this.builder.register_command(new Command("debug_exit", params => {})); - - this.builder.register_command( - new Command("debug_enter", params => { - let reason = params[1]; - if (reason !== "Breakpoint") { - this.exception = params[1]; - } else { - this.exception = ""; - } - this.godot_commands?.send_stack_dump_command(); - }) - ); - - this.builder.register_command( - new Command("stack_dump", params => { - let frames: Map[] = params; - this.trigger_breakpoint( - frames.map((sf, i) => { - return { - id: i, - thread_id: sf.get("id"), - file: sf.get("file"), - function: sf.get("function"), - line: sf.get("line") - }; - }) - ); - this.request_scene_tree(); - }) - ); - - this.builder.register_command( - new Command("output", params => { - if (!this.initial_output) { - this.initial_output = true; - this.request_scene_tree(); - } - params.forEach(line => { - this.output_channel?.appendLine(line); - }); - }) - ); - - this.builder.register_command( - new Command("error", params => { - params.forEach(param => {}); - }) - ); - - this.builder.register_command(new Command("performance", params => {})); - - this.builder.register_command( - new Command("message:inspect_object", params => { - let id = params[0]; - let class_name = params[1]; - let properties = params[2]; - - let callback = this.inspected_callbacks.shift(); - if (callback) { - callback(class_name, properties); - } - }) - ); - - this.builder.register_command( - new Command("message:scene_tree", params => { - if (this.tree_provider) { - let tree = SceneTreeBuilder.build(params); - this.tree_provider.fill_tree(tree); - } - }) - ); - - this.builder.register_command( - new Command("stack_frame_vars", params => { - let locals: any[] = []; - let members: any[] = []; - let globals: any[] = []; - - let local_count = (params[0] as number) * 2; - let member_count = params[1 + local_count] * 2; - let global_count = params[2 + local_count + member_count] * 2; - - if (local_count > 0) { - locals = params.slice(1, 1 + local_count); - } - if (member_count > 0) { - members = params.slice( - 2 + local_count, - 2 + local_count + member_count - ); - } - if (global_count > 0) { - globals = params.slice( - 3 + local_count + member_count, - 3 + local_count + member_count + global_count - ); - } - - this.pumpScope( - { - locals: locals, - members: members, - globals: globals - }, - project_path - ); - }) - ); - - this.server = net.createServer(connection => { - this.connection = connection; - this.godot_commands?.set_connection(connection); - - if (!launch_game_instance) { - this.breakpoints.forEach(bp => { - let path_to = path - .relative(this.project_path, bp.file) - .replace(/\\/g, "/"); - this.godot_commands?.send_set_breakpoint_command( - `res://${path_to}`, - bp.line - ); - }); - } - - connection.on("data", buffer => { - if (!this.parser || !this.builder) { - return; - } - - let split_buffers = this.split_buffer(buffer); - while(split_buffers.length > 0) { - let sub_buffer = split_buffers.shift() - let data = this.parser.get_buffer_dataset(sub_buffer, 0); - this.builder.parse_data(data.slice(1), error => { - console.log(error); - console.log(this.log_output); - }) - } - }); - - connection.on("close", hadError => { - if (hadError) { - this.send_event("terminated"); - } - }); - - connection.on("end", () => { - this.send_event("terminated"); - }); - - connection.on("error", error => { - console.error(error); - }); - - connection.on("drain", () => { - connection.resume(); - this.godot_commands?.set_can_write(true); - }); - }); - - this.server?.listen(port, address); - - if (launch_game_instance) { - let godot_path = utils.get_configuration( - "editor_path", - "godot" - ) as string; - let executable_line = `${godot_path} --path ${project_path} --remote-debug ${address}:${port}`; - executable_line += this.build_breakpoint_string( - breakpoints, - project_path - ); - let godot_exec = cp.exec(executable_line); - this.godot_pid = godot_exec.pid; - } - } - - private split_buffer(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; - } - - public step() { - this.godot_commands?.send_step_command(); - } - - public step_out() { - this.stepping_out = true; - this.next(); - } - - public stop() { - this.connection?.end(() => { - this.server?.close(); - if (this.godot_pid) { - TERMINATE(this.godot_pid, (error: string | undefined) => { - if (error) { - console.error(error); - } - }); - } - }); - this.send_event("terminated"); - } - - private build_breakpoint_string( - breakpoints: GodotBreakpoint[], - project: string - ): string { - let output = ""; - if (breakpoints.length > 0) { - output += " --breakpoints "; - - breakpoints.forEach(bp => { - let relative_path = path.relative(project, bp.file).replace(/\\/g, "/"); - if (relative_path.length !== 0) { - output += `res://${relative_path}:${bp.line},`; - } - }); - output = output.slice(0, -1); - } - - return output; - } - - private pumpScope( - scopes: { - locals: any[]; - members: any[]; - globals: any[]; - }, - projectPath: string - ) { - if (this.scope_callbacks.length > 0) { - let cb = this.scope_callbacks.shift(); - if (cb) { - let stack_files = this.stack_files.map(sf => { - return sf.replace("res://", `${projectPath}/`); - }); - cb(this.stack_level, stack_files, scopes); - } - } - } - - private send_event(event: string, ...args: any[]) { - setImmediate(_ => { - this.emitter.emit(event, ...args); - }); - } - - private trigger_breakpoint(stack_frames: GodotStackFrame[]) { - let continue_stepping = false; - let stack_count = stack_frames.length; - - let file = stack_frames[0].file.replace("res://", `${this.project_path}/`); - let line = stack_frames[0].line; - - if (this.stepping_out) { - let breakpoint = this.breakpoints.find( - k => k.file === file && k.line === line - ); - if (!breakpoint) { - if (this.stack_count > 1) { - continue_stepping = this.stack_count === stack_count; - } else { - let file_same = stack_frames[0].file === this.last_frame.file; - let func_same = stack_frames[0].function === this.last_frame.function; - let line_greater = stack_frames[0].line >= this.last_frame.line; - - continue_stepping = file_same && func_same && line_greater; - } - } - } - this.stack_count = stack_count; - this.last_frame = stack_frames[0]; - - if (continue_stepping) { - this.next(); - return; - } - - this.stepping_out = false; - - this.stack_files = stack_frames.map(sf => { - return sf.file; - }); - if (this.exception.length === 0) { - this.send_event("stopOnBreakpoint", stack_frames); - } else { - this.send_event("stopOnException", stack_frames, this.exception); - } - } -} diff --git a/src/debugger/debug_adapter.ts b/src/debugger/debug_adapter.ts index e557f3b..e90b625 100644 --- a/src/debugger/debug_adapter.ts +++ b/src/debugger/debug_adapter.ts @@ -1,3 +1,3 @@ -import { GodotDebugSession } from "./godot_debug"; +import { GodotDebugSession } from "./debug_session"; GodotDebugSession.run(GodotDebugSession); diff --git a/src/debugger/debug_runtime.ts b/src/debugger/debug_runtime.ts new file mode 100644 index 0000000..cb4e6c8 --- /dev/null +++ b/src/debugger/debug_runtime.ts @@ -0,0 +1,89 @@ +import { Mediator } from "./mediator"; +import { SceneTreeProvider } from "./scene_tree/scene_tree_provider"; +const path = require("path"); + +export interface GodotBreakpoint { + file: string; + id: number; + line: number; +} + +export interface GodotStackFrame { + file: string; + function: string; + id: number; + line: number; +} + +export interface GodotVariable { + name: string; + scope_path?: string; + sub_values?: GodotVariable[]; + value: any; +} + +export class GodotDebugData { + private breakpoint_id = 0; + private breakpoints: Map = new Map(); + + public last_frame: GodotStackFrame; + public last_frames: GodotStackFrame[] = []; + public project_path: string; + public scene_tree?: SceneTreeProvider; + public stack_count: number = 0; + public stack_files: string[] = []; + + 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 set_breakpoint(path_to: string, line: number) { + let bp = { + file: path_to.replace(/\\/g, "/"), + line: line, + id: this.breakpoint_id++, + }; + + let bps: GodotBreakpoint[] = this.breakpoints.get(bp.file); + if (!bps) { + bps = []; + this.breakpoints.set(bp.file, bps); + } + + bps.push(bp); + + let out_file = `res://${path.relative(this.project_path, bp.file)}`; + + Mediator.notify("set_breakpoint", [out_file.replace(/\\/g, "/"), line]); + } +} diff --git a/src/debugger/debug_session.ts b/src/debugger/debug_session.ts new file mode 100644 index 0000000..0df02da --- /dev/null +++ b/src/debugger/debug_session.ts @@ -0,0 +1,494 @@ +import { + LoggingDebugSession, + InitializedEvent, + Thread, + Source, + Breakpoint, +} from "vscode-debugadapter"; +import { DebugProtocol } from "vscode-debugprotocol"; +import { Mediator } from "./mediator"; +import { GodotDebugData, GodotVariable } from "./debug_runtime"; +import { ObjectId, RawObject } from "./variables/variants"; +import { ServerController } from "./server_controller"; +const { Subject } = require("await-notify"); +import fs = require("fs"); +import { SceneTreeProvider } from "./scene_tree/scene_tree_provider"; + +interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { + address: string; + launch_game_instance: boolean; + launch_scene: boolean; + port: number; + project: string; + scene_file: string; +} + +export class GodotDebugSession extends LoggingDebugSession { + private all_scopes: GodotVariable[]; + private configuration_done = new Subject(); + private controller?: ServerController; + private debug_data = new GodotDebugData(); + private exception = false; + private got_scope = new Subject(); + private ongoing_inspections: bigint[] = []; + private previous_inspections: bigint[] = []; + + 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 set_inspection(id: bigint, replacement: GodotVariable) { + let variables = this.all_scopes.filter( + (va) => va && va.value instanceof ObjectId && va.value.id === id + ); + + 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); + }); + + 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(); + } + } + + public set_scene_tree(scene_tree_provider: SceneTreeProvider) { + this.debug_data.scene_tree = scene_tree_provider; + } + + 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 configurationDoneRequest( + response: DebugProtocol.ConfigurationDoneResponse, + args: DebugProtocol.ConfigurationDoneArguments + ): void { + super.configurationDoneRequest(response, args); + this.configuration_done.notify(); + } + + protected continueRequest( + response: DebugProtocol.ContinueResponse, + args: DebugProtocol.ContinueArguments + ) { + if (!this.exception) { + response.body = { allThreadsContinued: true }; + Mediator.notify("continue"); + this.sendResponse(response); + } + } + + protected evaluateRequest( + response: DebugProtocol.EvaluateResponse, + args: DebugProtocol.EvaluateArguments + ) { + 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, + }; + } + } + } + + if (!response.body) { + response.body = { + result: "null", + variablesReference: 0, + }; + } + + 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(2000); + this.exception = false; + this.debug_data.project_path = args.project; + Mediator.notify("start", [ + args.project, + args.address, + args.port, + args.launch_game_instance, + args.launch_scene, + args.scene_file, + ]); + this.sendResponse(response); + } + + protected nextRequest( + response: DebugProtocol.NextResponse, + args: DebugProtocol.NextArguments + ) { + if (!this.exception) { + Mediator.notify("next"); + this.sendResponse(response); + } + } + + protected pauseRequest( + response: DebugProtocol.PauseResponse, + args: DebugProtocol.PauseArguments + ) { + if (!this.exception) { + Mediator.notify("break"); + this.sendResponse(response); + } + } + + protected async scopesRequest( + response: DebugProtocol.ScopesResponse, + args: DebugProtocol.ScopesArguments + ) { + while (this.ongoing_inspections.length > 0) { + await this.got_scope.wait(100); + } + Mediator.notify("get_scopes", [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 + ) { + let path = (args.source.path as string).replace(/\\/g, "/"); + let client_lines = args.lines || []; + + if (fs.existsSync(path)) { + let bps = this.debug_data.get_breakpoints(path); + let 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) { + this.debug_data.set_breakpoint(path, l); + } + }); + + bps = this.debug_data.get_breakpoints(path); + + 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.project_path}/${sf.file.replace("res://", "")}` + ), + }; + }), + }; + } + this.sendResponse(response); + } + + protected stepInRequest( + response: DebugProtocol.StepInResponse, + args: DebugProtocol.StepInArguments + ) { + if (!this.exception) { + Mediator.notify("step"); + this.sendResponse(response); + } + } + + protected stepOutRequest( + response: DebugProtocol.StepOutResponse, + args: DebugProtocol.StepOutArguments + ) { + if (!this.exception) { + Mediator.notify("step_out"); + this.sendResponse(response); + } + } + + protected terminateRequest( + response: DebugProtocol.TerminateResponse, + args: DebugProtocol.TerminateArguments + ) { + Mediator.notify("stop"); + 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 + ) { + let 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( + (sva) => + sva && sva.scope_path === va.scope_path && sva.name === va.name + ); + if (sva) { + return this.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); + } + + 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) + ) { + Mediator.notify("inspect_object", [va.value.id]); + this.ongoing_inspections.push(va.value.id); + } + } + }); + } + + 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}`; + if (variable.sub_values) { + variable.sub_values.forEach((va, i) => { + 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, + }; + } +} diff --git a/src/debugger/debugger_context.ts b/src/debugger/debugger_context.ts index 2710a0f..33301be 100644 --- a/src/debugger/debugger_context.ts +++ b/src/debugger/debugger_context.ts @@ -11,15 +11,16 @@ import { CancellationToken, ProviderResult, window, - commands + commands, } from "vscode"; -import { GodotDebugSession } from "./godot_debug"; +import { GodotDebugSession } from "./debug_session"; import fs = require("fs"); -import { SceneTreeProvider, SceneNode } from "./SceneTree/scene_tree_provider"; +import { SceneTreeProvider, SceneNode } from "./scene_tree/scene_tree_provider"; import { + RemoteProperty, InspectorProvider, - RemoteProperty -} from "./SceneTree/inspector_provider"; +} from "./scene_tree/inspector_provider"; +import { Mediator } from "./mediator"; export function register_debugger(context: ExtensionContext) { let provider = new GodotConfigurationProvider(); @@ -33,10 +34,7 @@ export function register_debugger(context: ExtensionContext) { let scene_tree_provider = new SceneTreeProvider(); window.registerTreeDataProvider("active-scene-tree", scene_tree_provider); - let factory = new GodotDebugAdapterFactory( - scene_tree_provider, - inspector_provider - ); + let factory = new GodotDebugAdapterFactory(scene_tree_provider); context.subscriptions.push( debug.registerDebugAdapterDescriptorFactory("godot", factory) ); @@ -45,8 +43,19 @@ export function register_debugger(context: ExtensionContext) { "godot-tool.debugger.inspect_node", (element: SceneNode | RemoteProperty) => { if (element instanceof SceneNode) { - factory.session.inspect_node( - element.label, + 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( @@ -55,39 +64,26 @@ export function register_debugger(context: ExtensionContext) { element.object_id, properties ); - } - ); - } else if (element instanceof RemoteProperty) { - factory.session.inspect_node( - element.label, - element.value.id, - (class_name, properties) => { - inspector_provider.fill_tree( - element.label, - class_name, - element.value.id, - properties - ); - } - ); + }, + ]); } } ); commands.registerCommand("godot-tool.debugger.refresh_scene_tree", () => { - factory.session.request_scene_tree(); + Mediator.notify("request_scene_tree", []); }); commands.registerCommand("godot-tool.debugger.refresh_inspector", () => { if (inspector_provider.has_tree()) { - factory.session.reinspect_node((name, class_name, properties) => { - inspector_provider.fill_tree( - name, - class_name, - factory.session.get_last_id(), - properties - ); - }); + 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); + }, + ]); } }); @@ -97,61 +93,75 @@ export function register_debugger(context: ExtensionContext) { 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 (new_parsed_value === NaN) { + 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 (new_parsed_value === NaN) { + return; + } + } else { + new_parsed_value = parseInt(value); + if (new_parsed_value === NaN) { + 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; } - } else { - new_parsed_value = parseInt(value); - if (new_parsed_value === NaN) { - return; - } - } - break; - case "boolean": - new_parsed_value = value.toLowerCase() === "true"; - break; - } - 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 - ); - factory.session.set_object_property( - property.object_id, - parents[idx].label, - changed_value - ); - } else { - factory.session.set_object_property( - property.object_id, - property.label, - new_parsed_value - ); - } - factory.session.reinspect_node((name, class_name, properties) => { - inspector_provider.fill_tree( - name, - class_name, - factory.session.get_last_id(), - properties - ); + 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 + ); + }, + ]); }); - }); } ); @@ -174,6 +184,7 @@ class GodotConfigurationProvider implements DebugConfigurationProvider { config.port = 6007; config.address = "127.0.0.1"; config.launch_game_instance = true; + config.launch_scene = false; } } @@ -194,17 +205,13 @@ class GodotConfigurationProvider implements DebugConfigurationProvider { class GodotDebugAdapterFactory implements DebugAdapterDescriptorFactory { public session: GodotDebugSession | undefined; - constructor( - private tree_provider: SceneTreeProvider, - private inspector_provider: InspectorProvider - ) {} + constructor(private scene_tree_provider: SceneTreeProvider) {} public createDebugAdapterDescriptor( session: DebugSession ): ProviderResult { this.session = new GodotDebugSession(); - this.inspector_provider.clean_up(); - this.session.set_tree_provider(this.tree_provider); + this.session.set_scene_tree(this.scene_tree_provider); return new DebugAdapterInlineImplementation(this.session); } diff --git a/src/debugger/godot_debug.ts b/src/debugger/godot_debug.ts deleted file mode 100644 index bfc2694..0000000 --- a/src/debugger/godot_debug.ts +++ /dev/null @@ -1,588 +0,0 @@ -import { - LoggingDebugSession, - InitializedEvent, - TerminatedEvent, - StoppedEvent, - Thread, - Source, - Breakpoint -} from "vscode-debugadapter"; -import { DebugProtocol } from "vscode-debugprotocol"; -import { window, OutputChannel } from "vscode"; -const { Subject } = require("await-notify"); -import { GodotDebugRuntime, GodotStackFrame } from "./godot_debug_runtime"; -import { VariableScope, VariableScopeBuilder } from "./variable_scope"; -import { SceneTreeProvider } from "./SceneTree/scene_tree_provider"; -import stringify from "./stringify"; -import fs = require("fs"); - -interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - address: string; - launch_game_instance: boolean; - port: number; - project: string; -} - -var output_channel: OutputChannel | undefined; - -export class GodotDebugSession extends LoggingDebugSession { - private static MAIN_THREAD_ID = 0; - - private configuration_done = new Subject(); - private current_stack_level = 0; - private excepted = false; - private have_scopes: (() => void)[] = []; - private last_frames: GodotStackFrame[] = []; - private last_inspection_id = -1; - private last_inspection_name = ""; - private runtime: GodotDebugRuntime; - private scope_builder: VariableScopeBuilder | undefined; - private tree_provider: SceneTreeProvider | undefined; - - public constructor() { - super(); - - if (!output_channel) { - output_channel = window.createOutputChannel("Godot"); - } else { - output_channel.clear(); - } - - this.setDebuggerLinesStartAt1(false); - this.setDebuggerColumnsStartAt1(false); - - this.runtime = new GodotDebugRuntime(); - - this.runtime.on("stopOnBreakpoint", frames => { - this.last_frames = frames; - this.sendEvent( - new StoppedEvent("breakpoint", GodotDebugSession.MAIN_THREAD_ID) - ); - }); - - this.runtime.on("stopOnException", (frames, exception) => { - this.last_frames = frames; - this.sendEvent( - new StoppedEvent( - "exception", - GodotDebugSession.MAIN_THREAD_ID, - exception - ) - ); - }); - - this.runtime.on("terminated", () => { - this.sendEvent(new TerminatedEvent(false)); - }); - } - - public dispose() {} - - public get_last_id(): number { - return this.last_inspection_id; - } - - public inspect_node( - object_name: string, - object_id: number, - inspected: (class_name: string, properties: any[]) => void - ) { - this.last_inspection_id = object_id; - this.last_inspection_name = object_name; - this.runtime.inspect_object(object_id, inspected); - } - - public reinspect_node( - callback: (name: string, class_name: string, properties: any[]) => void - ) { - this.inspect_node( - this.last_inspection_name, - this.last_inspection_id, - (class_name, properties) => { - callback(this.last_inspection_name, class_name, properties); - } - ); - } - - public request_scene_tree() { - this.runtime.request_scene_tree(); - } - - public set_object_property( - object_id: number, - label: string, - new_parsed_value: any - ) { - this.runtime.set_object_property(object_id, label, new_parsed_value); - } - - public set_tree_provider(tree_provider: SceneTreeProvider) { - this.tree_provider = tree_provider; - } - - protected configurationDoneRequest( - response: DebugProtocol.ConfigurationDoneResponse, - args: DebugProtocol.ConfigurationDoneArguments - ): void { - super.configurationDoneRequest(response, args); - - this.configuration_done.notify(); - } - - protected continueRequest( - response: DebugProtocol.ContinueResponse, - args: DebugProtocol.ContinueArguments - ): void { - if (this.excepted) { - return; - } - - response.body = { - allThreadsContinued: true - }; - - this.runtime.continue(); - - this.sendResponse(response); - } - - protected evaluateRequest( - response: DebugProtocol.EvaluateResponse, - args: DebugProtocol.EvaluateArguments - ) { - this.have_scopes.push(() => { - if (args.expression.match(/[^a-zA-Z0-9_\[\]\.]/g)) { - response.body = { - result: "not supported", - variablesReference: 0 - }; - this.sendResponse(response); - return; - } - - let is_self = args.expression.match(/^self\./); - let expression = args.expression - .replace(/[\[\]]/g, ".") - .replace(/\.$/, "") - .replace(/^self./, ""); - let variable: { name: string; value: any } | undefined; - let scope_keys = this.scope_builder.get_keys(this.current_stack_level); - let variable_id = -1; - for (let i = 0; i < scope_keys.length; ++i) { - let scopes = this.scope_builder.get( - this.current_stack_level, - scope_keys[i] - ); - - for (let l = is_self ? 1 : 0; l < 3; ++l) { - variable_id = scopes[l].get_id_for(expression); - if (variable_id !== -1) { - variable = scopes[l].get_variable(variable_id); - break; - } - } - - if (variable) { - break; - } - } - - if (!variable) { - response.body = { - result: "not available", - variablesReference: 0 - }; - - this.sendResponse(response); - return; - } - - let value_type_pair = stringify(variable.value); - - response.body = { - result: value_type_pair.value, - type: value_type_pair.type, - variablesReference: variable_id - }; - - this.sendResponse(response); - }); - if ( - this.scope_builder.size() > 0 && - this.scope_builder.get_keys(this.current_stack_level).length > 0 - ) { - this.have_scopes.shift()(); - } - } - - protected initializeRequest( - response: DebugProtocol.InitializeResponse, - args: DebugProtocol.InitializeRequestArguments - ): void { - 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; - - //TODO: Implement - response.body.supportsSetVariable = 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.excepted = false; - this.runtime.start( - args.project, - args.address, - args.port, - args.launch_game_instance, - output_channel, - this.tree_provider - ); - this.sendResponse(response); - } - - protected nextRequest( - response: DebugProtocol.NextResponse, - args: DebugProtocol.NextArguments - ): void { - if (this.excepted) { - return; - } - this.runtime.next(); - this.sendResponse(response); - } - - protected pauseRequest( - response: DebugProtocol.PauseResponse, - args: DebugProtocol.PauseArguments - ): void { - if (this.excepted) { - return; - } - this.runtime.break(); - this.sendResponse(response); - } - - protected scopesRequest( - response: DebugProtocol.ScopesResponse, - args: DebugProtocol.ScopesArguments - ): void { - this.runtime.getScope(args.frameId, (stack_level, stack_files, scopes) => { - this.current_stack_level = stack_level; - this.scope_builder = new VariableScopeBuilder( - this.runtime, - stack_level, - stack_files, - scopes, - this.have_scopes - ); - this.scope_builder.parse(over_scopes => { - response.body = { scopes: over_scopes }; - this.sendResponse(response); - }); - }); - } - - protected setBreakPointsRequest( - response: DebugProtocol.SetBreakpointsResponse, - args: DebugProtocol.SetBreakpointsArguments - ): void { - let path = (args.source.path as string).replace(/\\/g, "/"); - let client_lines = args.lines || []; - - if (fs.existsSync(path)) { - let bps = this.runtime.get_breakpoints(path); - let bp_lines = bps.map(bp => bp.line); - - bps.forEach(bp => { - if (client_lines.indexOf(bp.line) === -1) { - this.runtime.remove_breakpoint(path, bp.line); - } - }); - client_lines.forEach(l => { - if (bp_lines.indexOf(l) === -1) { - this.runtime.set_breakpoint(path, l); - } - }); - - bps = this.runtime.get_breakpoints(path); - - response.body = { - breakpoints: bps.map(bp => { - return new Breakpoint( - true, - bp.line, - 1, - new Source(bp.file.split("/").reverse()[0], bp.file, bp.id) - ); - }) - }; - - this.sendResponse(response); - } - } - - protected setExceptionBreakPointsRequest( - response: DebugProtocol.SetExceptionBreakpointsResponse, - args: DebugProtocol.SetExceptionBreakpointsArguments - ) { - this.excepted = true; - this.sendResponse(response); - } - - protected stackTraceRequest( - response: DebugProtocol.StackTraceResponse, - args: DebugProtocol.StackTraceArguments - ): void { - if (this.last_frames) { - response.body = { - totalFrames: this.last_frames.length, - stackFrames: this.last_frames.map(sf => { - return { - id: sf.id, - name: sf.function, - line: sf.line, - column: 1, - source: new Source( - sf.file, - `${this.runtime.getProject()}/${sf.file.replace("res://", "")}` - ) - }; - }) - }; - } - this.sendResponse(response); - } - - protected stepInRequest( - response: DebugProtocol.StepInResponse, - args: DebugProtocol.StepInArguments - ) { - if (this.excepted) { - return; - } - this.runtime.step(); - this.sendResponse(response); - } - - protected stepOutRequest( - response: DebugProtocol.StepOutResponse, - args: DebugProtocol.StepOutArguments - ) { - if (this.excepted) { - return; - } - - this.runtime.step_out(); - - this.sendResponse(response); - } - - protected terminateRequest( - response: DebugProtocol.TerminateResponse, - args: DebugProtocol.TerminateArguments - ) { - this.runtime.terminate(); - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - response.body = { - threads: [new Thread(GodotDebugSession.MAIN_THREAD_ID, "thread_1")] - }; - this.sendResponse(response); - } - - protected async variablesRequest( - response: DebugProtocol.VariablesResponse, - args: DebugProtocol.VariablesArguments, - request?: DebugProtocol.Request - ) { - let out_id = args.variablesReference; - let files = this.scope_builder.get_keys(this.current_stack_level); - - let out_scope_object = this.get_variable_scope(files, out_id); - let is_scope = out_scope_object.isScope; - let out_scope = out_scope_object.scope; - - if (out_scope) { - if (is_scope) { - let var_ids = out_scope.get_variable_ids(); - response.body = { - variables: this.parse_scope(var_ids, out_scope) - }; - } else { - let variable = out_scope.get_variable(out_id); - if (variable) { - let sub_variables = out_scope.get_sub_variables_for(out_id); - if (sub_variables) { - let ids = out_scope.get_variable_ids(); - let path_to = variable.name; - response.body = { - variables: [] - }; - - if (args.filter === "indexed") { - let count = args.count || 0; - for (let i = 0; i < count; i++) { - let name = `${path_to}.${i}`; - let id_index = ids.findIndex(id => { - let variable = out_scope?.get_variable(id); - return variable && name === variable.name; - }); - - response.body.variables.push( - this.get_variable_response( - name, - variable.value[i], - ids[id_index] - ) - ); - } - } else { - sub_variables.forEach(sv => { - let name = sv.name; - let id_index = ids.findIndex(id => { - let variable = out_scope?.get_variable(id); - return variable && name === variable.name; - }); - - response.body.variables.push( - this.get_variable_response(name, sv.value, ids[id_index]) - ); - }); - } - } else { - response.body = { - variables: [ - this.get_variable_response( - variable.name, - variable.value, - 0, - true - ) - ] - }; - } - } else { - response.body = { variables: [] }; - } - } - - this.sendResponse(response); - } - } - - private get_variable_response( - var_name: string, - var_value: any, - id: number, - skip_sub_var?: boolean - ) { - let value = ""; - let ref_id = 0; - let array_count = 0; - let type = ""; - if (!skip_sub_var) { - let output = stringify(var_value); - - value = output.value; - type = output.type; - ref_id = output.skip_id ? 0 : id; - } - return { - name: var_name.replace(/([a-zA-Z0-9_]+?\.)*/g, ""), - value: value, - variablesReference: ref_id, - indexedVariables: array_count, - type: type - }; - } - - private get_variable_scope(files: string[], scope_id: number) { - let out_scope: VariableScope | undefined; - let is_scope = false; - for (let i = 0; i < files.length; i++) { - let file = files[i]; - - let scopes = this.scope_builder.get(this.current_stack_level, file); - if (scopes) { - let index = scopes.findIndex(s => { - return s.id === scope_id; - }); - if (index !== -1) { - out_scope = scopes[index]; - is_scope = true; - break; - } else { - for (let l = 0; l < scopes.length; l++) { - let scope = scopes[l]; - let ids = scope.get_variable_ids(); - for (let k = 0; k < ids.length; k++) { - let id = ids[k]; - if (scope_id === id) { - out_scope = scope; - is_scope = false; - break; - } - } - } - } - } - } - - return { isScope: is_scope, scope: out_scope }; - } - - private parse_scope(var_ids: number[], out_scope: VariableScope) { - let output: DebugProtocol.Variable[] = []; - var_ids.forEach(id => { - let variable = out_scope?.get_variable(id); - if (variable && variable.name.indexOf(".") === -1) { - output.push( - this.get_variable_response(variable.name, variable.value, id) - ); - } - }); - - return output; - } -} diff --git a/src/debugger/godot_debug_runtime.ts b/src/debugger/godot_debug_runtime.ts deleted file mode 100644 index 5d5d3ad..0000000 --- a/src/debugger/godot_debug_runtime.ts +++ /dev/null @@ -1,177 +0,0 @@ -import vscode = require("vscode"); -import { EventEmitter } from "events"; -import { ServerController } from "./communication/server_controller"; -import { SceneTreeProvider } from "./SceneTree/scene_tree_provider"; -import { InspectorProvider } from "./SceneTree/inspector_provider"; - -export interface GodotBreakpoint { - file: string; - id: number; - line: number; -} - -export interface GodotStackFrame { - file: string; - function: string; - id: number; - line: number; -} - -export class GodotDebugRuntime extends EventEmitter { - private breakpointId = 0; - private breakpoints = new Map(); - private out: vscode.OutputChannel | undefined; - private paused = false; - private project = ""; - private server_controller: ServerController | undefined; - - constructor() { - super(); - } - - public break() { - if (this.paused) { - this.server_controller?.continue(); - } else { - this.server_controller?.break(); - } - } - - public continue() { - this.server_controller?.continue(); - } - - public getProject(): string { - return this.project; - } - - public getScope( - level: number, - callback?: ( - stackLevel: number, - stackFiles: string[], - scopes: { - locals: any[]; - members: any[]; - globals: any[]; - } - ) => void - ) { - this.server_controller?.get_scope(level, callback); - } - - public get_breakpoints(path: string): GodotBreakpoint[] { - let bps = this.breakpoints.get(path); - return bps ? bps : []; - } - - public inspect_object( - objectId: number, - inspected: (className: string, properties: any[]) => void - ) { - this.server_controller?.inspect_object(objectId, inspected); - } - - public next() { - this.server_controller?.next(); - } - - public remove_breakpoint(pathTo: string, line: number) { - let bps = this.breakpoints.get(pathTo); - 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(pathTo, bps); - this.server_controller?.remove_breakpoint( - bp.file.replace(new RegExp(`${this.project}/`), "res://"), - bp.line - ); - } - } - } - - public request_scene_tree() { - this.server_controller.request_scene_tree(); - } - - public set_object_property( - object_id: number, - label: string, - new_parsed_value: any - ) { - this.server_controller.set_object_property(object_id, label, new_parsed_value); - } - - public set_breakpoint(pathTo: string, line: number): GodotBreakpoint { - const BP = { - file: pathTo.replace(/\\/g, "/"), - line: line, - id: this.breakpointId++ - }; - - let bps = this.breakpoints.get(BP.file); - if (!bps) { - bps = new Array(); - this.breakpoints.set(BP.file, bps); - } - - bps.push(BP); - - this.server_controller?.set_breakpoint( - BP.file.replace(new RegExp(`${this.project}/`), "res://"), - line - ); - - return BP; - } - - public start( - project: string, - address: string, - port: number, - launchGameInstance: boolean, - out: vscode.OutputChannel, - tree_provider: SceneTreeProvider - ) { - this.out = out; - this.out.show(); - - this.project = project.replace(/\\/g, "/"); - if (this.project.match(/^[A-Z]:\//)) { - this.project = this.project[0].toLowerCase() + this.project.slice(1); - } - - this.server_controller = new ServerController( - this, - this.out, - tree_provider - ); - let breakpointList: GodotBreakpoint[] = []; - Array.from(this.breakpoints.values()).forEach(fbp => { - breakpointList = breakpointList.concat(fbp); - }); - this.server_controller.start( - project, - port, - address, - launchGameInstance, - breakpointList - ); - } - - public step() { - this.server_controller?.step(); - } - - public step_out() { - this.server_controller?.step_out(); - } - - public terminate() { - this.server_controller?.stop(); - } -} diff --git a/src/debugger/mediator.ts b/src/debugger/mediator.ts new file mode 100644 index 0000000..ed76954 --- /dev/null +++ b/src/debugger/mediator.ts @@ -0,0 +1,261 @@ +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"; + +let output: OutputChannel; + +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 constructor() { + if (!output) { + output = window.createOutputChannel("Godot"); + } else { + output.clear(); + } + } + + public static notify(event: string, parameters: any[] = []) { + switch (event) { + case "output": + let lines: string[] = parameters; + lines.forEach((line) => { + output?.appendLine(line); + }); + + if(!this.first_output) { + this.first_output = true; + this.controller?.send_request_scene_tree_command(); + } + 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], + this.debug_data + ); + break; + + case "debug_exit": + break; + + case "stop": + this.controller?.stop(); + this.session?.sendEvent(new TerminatedEvent(false)); + break; + + case "error": + this.controller?.set_exception(parameters[0]); + this.controller?.stop(); + this.session?.sendEvent(new TerminatedEvent(false)); + 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); + } +} diff --git a/src/debugger/scene_tree/inspector_provider.ts b/src/debugger/scene_tree/inspector_provider.ts new file mode 100644 index 0000000..1cd85e2 --- /dev/null +++ b/src/debugger/scene_tree/inspector_provider.ts @@ -0,0 +1,209 @@ +import { + TreeDataProvider, + EventEmitter, + Event, + ProviderResult, + TreeItem, + TreeItemCollapsibleState, +} from "vscode"; +import { GodotVariable } from "../debug_runtime"; +import { RawObject, ObjectId } from "../variables/variants"; + +export class InspectorProvider implements TreeDataProvider { + private _on_did_change_tree_data: EventEmitter< + RemoteProperty | undefined + > = new EventEmitter(); + private tree: RemoteProperty | undefined; + + public readonly onDidChangeTreeData: Event | undefined = this + ._on_did_change_tree_data.event; + + constructor() {} + + public clean_up() { + if (this.tree) { + this.tree = undefined; + this._on_did_change_tree_data.fire(); + } + } + + public fill_tree( + element_name: string, + class_name: string, + object_id: number, + variable: GodotVariable + ) { + this.tree = this.parse_variable(variable, object_id); + this.tree.label = element_name; + this.tree.collapsibleState = TreeItemCollapsibleState.Expanded; + this.tree.description = class_name; + this._on_did_change_tree_data.fire(); + } + + public getChildren( + element?: RemoteProperty + ): ProviderResult { + if (!this.tree) { + return Promise.resolve([]); + } + + if (!element) { + return Promise.resolve([this.tree]); + } else { + return Promise.resolve(element.properties); + } + } + + public getTreeItem(element: RemoteProperty): TreeItem | Thenable { + return element; + } + + public get_changed_value( + parents: RemoteProperty[], + property: RemoteProperty, + new_parsed_value: any + ) { + let idx = parents.length - 1; + let value = parents[idx].value; + if (Array.isArray(value)) { + let idx = parseInt(property.label); + if (idx < value.length) { + value[idx] = new_parsed_value; + } + } else if (value instanceof Map) { + value.set(property.parent.value.key, new_parsed_value); + } else if (value[property.label]) { + value[property.label] = new_parsed_value; + } + + return value; + } + + public get_top_id(): number { + if (this.tree) { + return this.tree.object_id; + } + return undefined; + } + + public get_top_name() { + if (this.tree) { + return this.tree.label; + } + return undefined; + } + + public has_tree() { + return this.tree !== undefined; + } + + private parse_variable(va: GodotVariable, object_id?: number) { + let value = va.value; + let rendered_value = ""; + + 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}]`; + } else if (value instanceof Map) { + if (value instanceof RawObject) { + rendered_value = `${value.class_name}`; + } else { + rendered_value = `Dictionary[${value.size}]`; + } + } else { + rendered_value = `${value.type_name()}${value.stringify_value()}`; + } + } + + let child_props: RemoteProperty[] = []; + + if (value) { + let 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 = + typeof va["rendered_value"] === "function" + ? va.rendered_value() + : `${va}`; + let 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( + va.name, + value, + object_id, + child_props, + child_props.length === 0 + ? TreeItemCollapsibleState.None + : TreeItemCollapsibleState.Collapsed + ); + out_prop.description = rendered_value; + out_prop.properties.forEach((prop) => { + prop.parent = out_prop; + }); + out_prop.description = rendered_value; + + if (value instanceof ObjectId) { + out_prop.contextValue = "remote_object"; + out_prop.object_id = Number(value.id); + } else if ( + typeof value === "number" || + typeof value === "bigint" || + typeof value === "boolean" || + typeof value === "string" + ) { + out_prop.contextValue = "editable_value"; + } else if ( + Array.isArray(value) || + (value instanceof Map && value instanceof RawObject === false) + ) { + out_prop.properties.forEach((prop) => (prop.changes_parent = true)); + } + + return out_prop; + } +} + +export class RemoteProperty extends TreeItem { + public changes_parent?: boolean; + public parent?: RemoteProperty; + + constructor( + public label: string, + public value: any, + public object_id: number, + public properties: RemoteProperty[], + public collapsibleState?: TreeItemCollapsibleState + ) { + super(label, collapsibleState); + } +} + +export class RemoteObject extends RemoteProperty {} diff --git a/src/debugger/SceneTree/scene_tree_provider.ts b/src/debugger/scene_tree/scene_tree_provider.ts similarity index 98% rename from src/debugger/SceneTree/scene_tree_provider.ts rename to src/debugger/scene_tree/scene_tree_provider.ts index 91c95ac..c54087f 100644 --- a/src/debugger/SceneTree/scene_tree_provider.ts +++ b/src/debugger/scene_tree/scene_tree_provider.ts @@ -4,7 +4,7 @@ import { Event, ProviderResult, TreeItem, - TreeItemCollapsibleState + TreeItemCollapsibleState, } from "vscode"; import path = require("path"); import fs = require("fs"); @@ -121,7 +121,7 @@ export class SceneNode extends TreeItem { this.iconPath = { light: light, - dark: dark + dark: dark, }; } } diff --git a/src/debugger/server_controller.ts b/src/debugger/server_controller.ts new file mode 100644 index 0000000..5b0c098 --- /dev/null +++ b/src/debugger/server_controller.ts @@ -0,0 +1,296 @@ +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; + + 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, + debug_data: GodotDebugData + ) { + this.debug_data = debug_data; + + 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); + + if (launch_instance) { + let godot_path: string = utils.get_configuration("editor_path", "godot"); + let executable_line = `"${godot_path}" --path "${project_path}" --remote-debug ${address}:${port}`; + if (launch_scene) { + let filename = ""; + if (scene_file) { + filename = scene_file; + } else { + filename = window.activeTextEditor.document.fileName; + } + if (path.extname(filename).toLowerCase() === ".tscn") { + executable_line += ` ${path.relative(project_path, filename)}`; + } else { + window.showErrorMessage("Active file is not a TSCN file."); + Mediator.notify("stop"); + return; + } + } + executable_line += this.breakpoint_string( + debug_data.get_all_breakpoints(), + project_path + ); + let godot_exec = cp.exec(executable_line); + this.godot_pid = godot_exec.pid; + } + } + + 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?.end(() => { + this.server.close(); + this.server = undefined; + }); + if (this.godot_pid) { + TERMINATE(this.godot_pid, (error: string | undefined) => { + if (error) { + Mediator.notify("error", [error]); + } + }); + 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; + } +} diff --git a/src/debugger/stringify.ts b/src/debugger/stringify.ts deleted file mode 100644 index d6cca5f..0000000 --- a/src/debugger/stringify.ts +++ /dev/null @@ -1,91 +0,0 @@ -export default function stringify( - var_value: any, - decimal_precision: number = 4 -) { - let type = ""; - let value = ""; - let skip_id = true; - if (typeof var_value === "number" && !Number.isInteger(var_value)) { - value = String( - +Number.parseFloat(no_exponents(var_value)).toFixed(decimal_precision) - ); - type = "Float"; - } else if (Array.isArray(var_value)) { - value = "Array"; - type = "Array"; - skip_id = false; - } else if (var_value instanceof Map) { - value = "Dictionary"; - type = "Dictionary"; - skip_id = false; - } else if (typeof var_value === "object") { - skip_id = false; - if (var_value.__type__) { - if (var_value.__type__ === "Object") { - skip_id = true; - } - if (var_value.__render__) { - value = var_value.__render__(); - } else { - value = var_value.__type__; - } - type = var_value.__type__; - } else { - value = "Object"; - } - } else { - if (var_value) { - if (Number.isInteger(var_value)) { - type = "Int"; - value = `${var_value}`; - } else if (typeof var_value === "string") { - type = "String"; - value = String(var_value); - } else if (typeof var_value === "boolean") { - type = "Bool"; - value = "true"; - } else { - type = "unknown"; - value = `${var_value}`; - } - } else { - if (Number.isInteger(var_value)) { - type = "Int"; - value = "0"; - } else if (typeof var_value === "boolean") { - type = "Bool"; - value = "false"; - } else { - type = "unknown"; - value = "null"; - } - } - } - - return { type: type, value: value, skip_id: skip_id }; -} - -function no_exponents(value: number): string { - let data = String(value).split(/[eE]/); - if (data.length === 1) { - return data[0]; - } - - let z = "", - sign = value < 0 ? "-" : ""; - let str = data[0].replace(".", ""); - let mag = Number(data[1]) + 1; - - if (mag < 0) { - z = sign + "0."; - while (mag++) { - z += "0"; - } - return z + str.replace(/^\-/, ""); - } - mag -= str.length; - while (mag--) { - z += 0; - } - return str + z; -} diff --git a/src/debugger/variable_scope.ts b/src/debugger/variable_scope.ts deleted file mode 100644 index a055c9a..0000000 --- a/src/debugger/variable_scope.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { DebugProtocol } from "vscode-debugprotocol"; - -import { GodotDebugRuntime } from "./godot_debug_runtime"; -import stringify from "./stringify"; - -export class VariableScopeBuilder { - private inspect_callback: (() => void) | undefined; - private inspected: number[] = []; - private inspected_cache = new Map< - number, - { class_name: string; properties: any[] } - >(); - private over_scopes: DebugProtocol.Scope[]; - private scope_id = 1; - private scopes = new Map>(); - - constructor( - private runtime: GodotDebugRuntime, - private stack_level: number, - private stack_files: string[], - private raw_scopes: { locals: any[]; members: any[]; globals: any[] }, - private have_scopes: (() => void)[] = [] - ) {} - - public get(level: number, file: string) { - return this.scopes.get(level).get(file); - } - - public get_keys(level: number) { - return Array.from(this.scopes.get(level).keys()); - } - - public parse(callback: (over_scopes: DebugProtocol.Scope[]) => void) { - let file = this.stack_files[this.stack_level]; - - let file_scopes: VariableScope[] = []; - - let local_scope = new VariableScope(this.scope_id++); - let member_scope = new VariableScope(this.scope_id++); - let global_scope = new VariableScope(this.scope_id++); - - file_scopes.push(local_scope); - file_scopes.push(member_scope); - file_scopes.push(global_scope); - - this.scopes.set( - this.stack_level, - new Map([[file, file_scopes]]) - ); - - let out_local_scope: DebugProtocol.Scope = { - name: "Locals", - namedVariables: this.raw_scopes.locals.length / 2, - presentationHint: "locals", - expensive: false, - variablesReference: local_scope.id - }; - - for (let i = 0; i < this.raw_scopes.locals.length; i += 2) { - let name = this.raw_scopes.locals[i]; - let value = this.raw_scopes.locals[i + 1]; - - this.drill_scope( - local_scope, - { - name: name, - value: value ? value : undefined - }, - !value && typeof value === "number" - ); - } - - let out_member_scope: DebugProtocol.Scope = { - name: "Members", - namedVariables: this.raw_scopes.members.length / 2, - presentationHint: "locals", - expensive: false, - variablesReference: member_scope.id - }; - - for (let i = 0; i < this.raw_scopes.members.length; i += 2) { - let name = this.raw_scopes.members[i]; - let value = this.raw_scopes.members[i + 1]; - - this.drill_scope( - member_scope, - { name: name, value: value }, - !value && typeof value === "number" - ); - } - - let out_global_scope: DebugProtocol.Scope = { - name: "Globals", - namedVariables: this.raw_scopes.globals.length / 2, - presentationHint: "locals", - expensive: false, - variablesReference: global_scope.id - }; - - for (let i = 0; i < this.raw_scopes.globals.length; i += 2) { - let name = this.raw_scopes.globals[i]; - let value = this.raw_scopes.globals[i + 1]; - - this.drill_scope( - global_scope, - { name: name, value: value }, - !value && typeof value === "number" - ); - } - - this.over_scopes = [out_local_scope, out_member_scope, out_global_scope]; - - if (this.inspected.length === 0) { - while (this.have_scopes.length > 0) { - this.have_scopes.shift()(); - } - callback(this.over_scopes); - } else { - this.inspect_callback = () => { - while (this.have_scopes.length > 0) { - this.have_scopes.shift()(); - } - callback(this.over_scopes); - }; - } - } - - public size() { - return this.scopes.size; - } - - private drill_scope( - scope: VariableScope, - variable: any, - is_zero_number?: boolean - ) { - if (is_zero_number) { - variable.value = 0; - } - let id = scope.get_id_for(variable.name); - if (id === -1) { - id = this.scope_id++; - } - scope.set_variable(variable.name, variable.value, id); - if (Array.isArray(variable.value) || variable.value instanceof Map) { - let length = 0; - let values: any[]; - if (variable.value instanceof Map) { - length = variable.value.size; - let keys = Array.from(variable.value.keys()); - values = keys.map(key => { - let value = variable.value.get(key); - let stringified_key = stringify(key).value; - - return { - __type__: "Pair", - key: key, - value: value, - __render__: () => stringified_key - }; - }); - variable.value = values; - } else { - length = variable.value.length; - values = variable.value; - } - for (let i = 0; i < length; i++) { - let name = `${variable.name}.${i}`; - scope.set_sub_variable_for(id, name, values[i]); - this.drill_scope(scope, { - name: name, - value: values[i] - }); - } - } else if (typeof variable.value === "object") { - if (variable.value.__type__ && variable.value.__type__ === "Object") { - if (!this.inspected_cache.has(id)) { - if (this.inspected.indexOf(id) === -1) { - this.inspected.push(id); - this.runtime.inspect_object( - variable.value.id, - (class_name, properties) => { - this.inspected_cache.set(id, { - class_name: class_name, - properties: properties - }); - this.parse_deeper(variable, scope, id, class_name, properties); - } - ); - } - } else { - let cached = this.inspected_cache.get(id); - this.parse_deeper( - variable, - scope, - id, - cached.class_name, - cached.properties - ); - } - } else { - for (const PROP in variable.value) { - if (PROP !== "__type__" && PROP !== "__render__") { - let name = `${variable.name}.${PROP}`; - scope.set_sub_variable_for(id, name, variable.value[PROP]); - this.drill_scope(scope, { - name: name, - value: variable.value[PROP] - }); - } - } - } - } - } - - private parse_deeper( - variable: any, - scope: VariableScope, - id: number, - class_name: string, - properties: any[][] - ) { - variable.value.__type__ = class_name; - let start_index = 0; - variable.value.__render__ = () => `${class_name}`; - let relevant_properties = properties.slice(start_index + 1).filter(p => { - if (!p[5]) { - return Number.isInteger(p[5]); - } - - return true; - }); - relevant_properties.forEach(p => { - let sub_name = `${variable.name}.${p[0]}`; - scope.set_sub_variable_for(id, sub_name, p[5]); - this.drill_scope(scope, { name: sub_name, value: p[5] }); - }); - - let inspected_idx = this.inspected.indexOf(variable.value.id); - if (inspected_idx !== -1) { - this.inspected.splice(inspected_idx, 1); - } - - if (this.inspected.length === 0 && this.inspect_callback) { - this.inspect_callback(); - } - } -} - -export class VariableScope { - private sub_variables = new Map(); - private variables = new Map(); - - public readonly id: number; - - constructor(id: number) { - this.id = id; - } - - public get_id_for(name: string) { - let ids = Array.from(this.variables.keys()); - return ( - ids.find(v => { - let var_name = this.variables.get(v).name; - return var_name === name; - }) || -1 - ); - } - - public get_sub_variable_for(name: string, id: number) { - let sub_variables = this.sub_variables.get(id); - if (sub_variables) { - let index = sub_variables.findIndex(sv => { - return sv.name === name; - }); - if (index !== -1) { - return sub_variables[index]; - } - } - - return undefined; - } - - public get_sub_variables_for(id: number) { - return this.sub_variables.get(id); - } - - public get_variable(id: number): { name: string; value: any } | undefined { - return this.variables.get(id); - } - - public get_variable_ids() { - return Array.from(this.variables.keys()); - } - - public set_sub_variable_for(variable_id: number, name: string, value: any) { - let sub_variables = this.sub_variables.get(variable_id); - if (!sub_variables) { - sub_variables = []; - this.sub_variables.set(variable_id, sub_variables); - } - - let index = sub_variables.findIndex(sv => { - return sv.name === name; - }); - - if (index === -1) { - sub_variables.push({ name: name, value: value }); - } - } - - public set_variable(name: string, value: any, id: number) { - let variable = { name: name, value: value }; - this.variables.set(id, variable); - } -} diff --git a/src/debugger/variables/variant_decoder.ts b/src/debugger/variables/variant_decoder.ts new file mode 100644 index 0000000..2fe012c --- /dev/null +++ b/src/debugger/variables/variant_decoder.ts @@ -0,0 +1,392 @@ +import { + GDScriptTypes, + BufferModel, + Vector3, + Vector2, + Basis, + AABB, + Color, + NodePath, + ObjectId, + Plane, + Quat, + Rect2, + Transform, + Transform2D, + RawObject, +} from "./variants"; + +export class VariantDecoder { + public decode_variant(model: BufferModel) { + let type = this.decode_UInt32(model); + switch (type & 0xff) { + case GDScriptTypes.BOOL: + return this.decode_UInt32(model) !== 0; + case GDScriptTypes.INT: + if (type & (1 << 16)) { + return this.decode_Int64(model); + } else { + return this.decode_Int32(model); + } + case GDScriptTypes.REAL: + if (type & (1 << 16)) { + return this.decode_Double(model); + } else { + return this.decode_Float(model); + } + case GDScriptTypes.STRING: + return this.decode_String(model); + case GDScriptTypes.VECTOR2: + return this.decode_Vector2(model); + case GDScriptTypes.RECT2: + return this.decode_Rect2(model); + case GDScriptTypes.VECTOR3: + return this.decode_Vector3(model); + case GDScriptTypes.TRANSFORM2D: + return this.decode_Transform2D(model); + case GDScriptTypes.PLANE: + return this.decode_Plane(model); + case GDScriptTypes.QUAT: + return this.decode_Quat(model); + case GDScriptTypes.AABB: + return this.decode_AABB(model); + case GDScriptTypes.BASIS: + return this.decode_Basis(model); + case GDScriptTypes.TRANSFORM: + return this.decode_Transform(model); + case GDScriptTypes.COLOR: + return this.decode_Color(model); + case GDScriptTypes.NODE_PATH: + return this.decode_NodePath(model); + case GDScriptTypes.OBJECT: + if (type & (1 << 16)) { + return this.decode_Object_id(model); + } else { + return this.decode_Object(model); + } + case GDScriptTypes.DICTIONARY: + return this.decode_Dictionary(model); + case GDScriptTypes.ARRAY: + return this.decode_Array(model); + case GDScriptTypes.POOL_BYTE_ARRAY: + return this.decode_PoolByteArray(model); + case GDScriptTypes.POOL_INT_ARRAY: + return this.decode_PoolIntArray(model); + case GDScriptTypes.POOL_REAL_ARRAY: + return this.decode_PoolFloatArray(model); + case GDScriptTypes.POOL_STRING_ARRAY: + return this.decode_PoolStringArray(model); + case GDScriptTypes.POOL_VECTOR2_ARRAY: + return this.decode_PoolVector2Array(model); + case GDScriptTypes.POOL_VECTOR3_ARRAY: + return this.decode_PoolVector3Array(model); + case GDScriptTypes.POOL_COLOR_ARRAY: + return this.decode_PoolColorArray(model); + default: + return undefined; + } + } + + public get_dataset(buffer: Buffer, offset: number) { + let len = buffer.readUInt32LE(offset); + let model: BufferModel = { + buffer: buffer, + offset: offset + 4, + len: len, + }; + + let output = []; + output.push(len + 4); + do { + let value = this.decode_variant(model); + output.push(value); + } while (model.len > 0); + + return output; + } + + private decode_AABB(model: BufferModel) { + return new AABB(this.decode_Vector3(model), this.decode_Vector3(model)); + } + + private decode_Array(model: BufferModel) { + let output: Array = []; + + let count = this.decode_UInt32(model); + + for (let i = 0; i < count; i++) { + let value = this.decode_variant(model); + output.push(value); + } + + return output; + } + + private decode_Basis(model: BufferModel) { + return new Basis( + this.decode_Vector3(model), + this.decode_Vector3(model), + this.decode_Vector3(model) + ); + } + + private decode_Color(model: BufferModel) { + let rgb = this.decode_Vector3(model); + let a = this.decode_Float(model); + + return new Color(rgb.x, rgb.y, rgb.z, a); + } + + private decode_Dictionary(model: BufferModel) { + let output = new Map(); + + let count = this.decode_UInt32(model); + for (let i = 0; i < count; i++) { + let key = this.decode_variant(model); + let value = this.decode_variant(model); + output.set(key, value); + } + + return output; + } + + private decode_Double(model: BufferModel) { + let d = model.buffer.readDoubleLE(model.offset); + + model.offset += 8; + model.len -= 8; + + return d; // + (d < 0 ? -1e-10 : 1e-10); + } + + private decode_Float(model: BufferModel) { + let f = model.buffer.readFloatLE(model.offset); + + model.offset += 4; + model.len -= 4; + + return f; // + (f < 0 ? -1e-10 : 1e-10); + } + + private decode_Int32(model: BufferModel) { + let u = model.buffer.readInt32LE(model.offset); + + model.len -= 4; + model.offset += 4; + + return u; + } + + 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); + + model.len -= 8; + model.offset += 8; + + return u; + } + + private decode_NodePath(model: BufferModel) { + let 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; + 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[] = []; + for (let i = 0; i < total; i++) { + let 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) { + let class_name = this.decode_String(model); + let prop_count = this.decode_UInt32(model); + let 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); + output.set(name, value); + } + + return output; + } + + private decode_Object_id(model: BufferModel) { + let 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); + + return new Plane(x, y, z, d); + } + + private decode_PoolByteArray(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: number[] = []; + for (let i = 0; i < count; i++) { + output.push(model.buffer.readUInt8(model.offset)); + model.offset++; + model.len--; + } + + return output; + } + + private decode_PoolColorArray(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: Color[] = []; + for (let i = 0; i < count; i++) { + output.push(this.decode_Color(model)); + } + + return output; + } + + private decode_PoolFloatArray(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: number[] = []; + for (let i = 0; i < count; i++) { + output.push(this.decode_Float(model)); + } + + return output; + } + + private decode_PoolIntArray(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: number[] = []; + for (let i = 0; i < count; i++) { + output.push(this.decode_Int32(model)); + } + + return output; + } + + private decode_PoolStringArray(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: string[] = []; + for (let i = 0; i < count; i++) { + output.push(this.decode_String(model)); + } + + return output; + } + + private decode_PoolVector2Array(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: Vector2[] = []; + for (let i = 0; i < count; i++) { + output.push(this.decode_Vector2(model)); + } + + return output; + } + + private decode_PoolVector3Array(model: BufferModel) { + let count = this.decode_UInt32(model); + let output: Vector3[] = []; + for (let i = 0; i < count; i++) { + output.push(this.decode_Vector3(model)); + } + + return output; + } + + 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); + + return new Quat(x, y, z, w); + } + + private decode_Rect2(model: BufferModel) { + return new Rect2(this.decode_Vector2(model), this.decode_Vector2(model)); + } + + private decode_String(model: BufferModel) { + let len = this.decode_UInt32(model); + let pad = 0; + if (len % 4 !== 0) { + pad = 4 - (len % 4); + } + + let str = model.buffer.toString("utf8", model.offset, model.offset + len); + len += pad; + + model.offset += len; + model.len -= len; + + return str; + } + + private decode_Transform(model: BufferModel) { + return new Transform(this.decode_Basis(model), this.decode_Vector3(model)); + } + + private decode_Transform2D(model: BufferModel) { + return new Transform2D( + this.decode_Vector2(model), + this.decode_Vector2(model), + this.decode_Vector2(model) + ); + } + + 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); + + 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); + + return new Vector3(x, y, z); + } +} diff --git a/src/debugger/variables/variant_encoder.ts b/src/debugger/variables/variant_encoder.ts new file mode 100644 index 0000000..9da4f95 --- /dev/null +++ b/src/debugger/variables/variant_encoder.ts @@ -0,0 +1,359 @@ +import { + GDScriptTypes, + BufferModel, + Vector3, + Vector2, + Basis, + AABB, + Color, + Plane, + Quat, + Rect2, + Transform, + Transform2D, +} from "./variants"; + +export class VariantEncoder { + public encode_variant( + value: + | number + | bigint + | boolean + | string + | Map + | Array + | object + | undefined, + model?: BufferModel + ) { + if ( + typeof value === "number" && + Number.isInteger(value) && + (value > 2147483647 || value < -2147483648) + ) { + value = BigInt(value); + } + + if (!model) { + let size = this.size_variant(value); + let buffer = Buffer.alloc(size + 4); + model = { + buffer: buffer, + offset: 0, + len: 0, + }; + this.encode_UInt32(size, model); + } + + switch (typeof value) { + case "number": + { + let is_integer = Number.isInteger(value); + if (is_integer) { + this.encode_UInt32(GDScriptTypes.INT, model); + this.encode_UInt32(value, model); + } else { + this.encode_UInt32(GDScriptTypes.REAL | (1 << 16), model); + this.encode_Float(value, model); + } + } + break; + case "bigint": + this.encode_UInt32(GDScriptTypes.INT | (1 << 16), 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 Vector2) { + this.encode_UInt32(GDScriptTypes.VECTOR2, model); + this.encode_Vector2(value, model); + } else if (value instanceof Rect2) { + this.encode_UInt32(GDScriptTypes.RECT2, model); + this.encode_Rect2(value, model); + } else if (value instanceof Vector3) { + this.encode_UInt32(GDScriptTypes.VECTOR3, model); + this.encode_Vector3(value, model); + } else if (value instanceof Transform2D) { + this.encode_UInt32(GDScriptTypes.TRANSFORM2D, model); + this.encode_Transform2D(value, model); + } else if (value instanceof Plane) { + this.encode_UInt32(GDScriptTypes.PLANE, model); + this.encode_Plane(value, model); + } else if (value instanceof Quat) { + this.encode_UInt32(GDScriptTypes.QUAT, model); + this.encode_Quat(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 Transform) { + this.encode_UInt32(GDScriptTypes.TRANSFORM, model); + this.encode_Transform(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) { + let 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_Float(value.r, model); + this.encode_Float(value.g, model); + this.encode_Float(value.b, model); + this.encode_Float(value.a, model); + } + + private encode_Dictionary(dict: Map, model: BufferModel) { + let size = dict.size; + this.encode_UInt32(size, model); + let keys = Array.from(dict.keys()); + keys.forEach((key) => { + let value = dict.get(key); + this.encode_variant(key, model); + this.encode_variant(value, model); + }); + } + + private encode_Double(value: number, model: BufferModel) { + model.buffer.writeDoubleLE(value, model.offset); + model.offset += 8; + } + + private encode_Float(value: number, model: BufferModel) { + model.buffer.writeFloatLE(value, model.offset); + model.offset += 4; + } + + private encode_Plane(value: Plane, model: BufferModel) { + this.encode_Float(value.x, model); + this.encode_Float(value.y, model); + this.encode_Float(value.z, model); + this.encode_Float(value.d, model); + } + + private encode_Quat(value: Quat, model: BufferModel) { + this.encode_Float(value.x, model); + this.encode_Float(value.y, model); + this.encode_Float(value.z, model); + this.encode_Float(value.w, model); + } + + private encode_Rect2(value: Rect2, model: BufferModel) { + this.encode_Vector2(value.position, model); + this.encode_Vector2(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_Transform(value: Transform, 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_UInt32(int: number, model: BufferModel) { + model.buffer.writeUInt32LE(int, model.offset); + model.offset += 4; + } + + 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); + } + + private encode_Vector2(value: Vector2, model: BufferModel) { + this.encode_Float(value.x, model); + this.encode_Float(value.y, model); + } + + private encode_Vector3(value: Vector3, model: BufferModel) { + this.encode_Float(value.x, model); + this.encode_Float(value.y, model); + this.encode_Float(value.z, model); + } + + private size_Bool(): number { + return this.size_UInt32(); + } + + private size_Dictionary(dict: Map): number { + let size = this.size_UInt32(); + let keys = Array.from(dict.keys()); + keys.forEach((key) => { + let 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[] + | 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: + if (Array.isArray(value)) { + size += this.size_array(value); + break; + } else if (value instanceof Map) { + size += this.size_Dictionary(value); + break; + } else { + switch (value["__type__"]) { + case "Vector2": + size += this.size_UInt32() * 2; + break; + case "Rect2": + size += this.size_UInt32() * 4; + break; + case "Vector3": + size += this.size_UInt32() * 3; + break; + case "Transform2D": + size += this.size_UInt32() * 6; + break; + case "Plane": + size += this.size_UInt32() * 4; + break; + case "Quat": + size += this.size_UInt32() * 4; + break; + case "AABB": + size += this.size_UInt32() * 6; + break; + case "Basis": + size += this.size_UInt32() * 9; + break; + case "Transform": + size += this.size_UInt32() * 12; + break; + case "Color": + size += this.size_UInt32() * 4; + break; + } + } + } + + return size; + } +} diff --git a/src/debugger/variables/variants.ts b/src/debugger/variables/variants.ts new file mode 100644 index 0000000..2651363 --- /dev/null +++ b/src/debugger/variables/variants.ts @@ -0,0 +1,332 @@ +import { GodotVariable } from "../debug_runtime"; + +export enum GDScriptTypes { + NIL, + + // atomic types + BOOL, + INT, + REAL, + STRING, + + // math types + + VECTOR2, // 5 + RECT2, + VECTOR3, + TRANSFORM2D, + PLANE, + QUAT, // 10 + AABB, + BASIS, + TRANSFORM, + + // misc types + COLOR, + NODE_PATH, // 15 + _RID, + OBJECT, + DICTIONARY, + ARRAY, + + // arrays + POOL_BYTE_ARRAY, // 20 + POOL_INT_ARRAY, + POOL_REAL_ARRAY, + POOL_STRING_ARRAY, + POOL_VECTOR2_ARRAY, + POOL_VECTOR3_ARRAY, // 25 + POOL_COLOR_ARRAY, + + VARIANT_MAX, +} + +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 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 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 { + 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 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 Transform 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"; + } +} diff --git a/src/debugger/variant_parser.ts b/src/debugger/variant_parser.ts deleted file mode 100644 index 1817fef..0000000 --- a/src/debugger/variant_parser.ts +++ /dev/null @@ -1,905 +0,0 @@ -enum GDScriptTypes { - NIL, - - // atomic types - BOOL, - INT, - REAL, - STRING, - - // math types - - VECTOR2, // 5 - RECT2, - VECTOR3, - TRANSFORM2D, - PLANE, - QUAT, // 10 - AABB, - BASIS, - TRANSFORM, - - // misc types - COLOR, - NODE_PATH, // 15 - _RID, - OBJECT, - DICTIONARY, - ARRAY, - - // arrays - POOL_BYTE_ARRAY, // 20 - POOL_INT_ARRAY, - POOL_REAL_ARRAY, - POOL_STRING_ARRAY, - POOL_VECTOR2_ARRAY, - POOL_VECTOR3_ARRAY, // 25 - POOL_COLOR_ARRAY, - - VARIANT_MAX -} - -interface BufferModel { - buffer: Buffer; - len: number; - offset: number; -} - -export class VariantParser { - public decode_variant(model: BufferModel) { - let type = this.decode_UInt32(model); - switch (type & 0xff) { - case GDScriptTypes.BOOL: - return this.decode_UInt32(model) !== 0; - case GDScriptTypes.INT: - if (type & (1 << 16)) { - return this.decode_Int64(model); - } else { - return this.decode_Int32(model); - } - case GDScriptTypes.REAL: - if (type & (1 << 16)) { - return this.decode_Double(model); - } else { - return this.decode_Float(model); - } - case GDScriptTypes.STRING: - return this.decode_String(model); - case GDScriptTypes.VECTOR2: - return this.decode_Vector2(model); - case GDScriptTypes.RECT2: - return this.decode_Rect2(model); - case GDScriptTypes.VECTOR3: - return this.decode_Vector3(model); - case GDScriptTypes.TRANSFORM2D: - return this.decode_Transform2D(model); - case GDScriptTypes.PLANE: - return this.decode_Plane(model); - case GDScriptTypes.QUAT: - return this.decode_Quat(model); - case GDScriptTypes.AABB: - return this.decode_AABB(model); - case GDScriptTypes.BASIS: - return this.decode_Basis(model); - case GDScriptTypes.TRANSFORM: - return this.decode_Transform(model); - case GDScriptTypes.COLOR: - return this.decode_Color(model); - case GDScriptTypes.NODE_PATH: - return this.decode_NodePath(model); - case GDScriptTypes.OBJECT: - if (type & (1 << 16)) { - return this.decode_Object_id(model); - } else { - return this.decode_Object(model); - } - case GDScriptTypes.DICTIONARY: - return this.decode_Dictionary(model); - case GDScriptTypes.ARRAY: - return this.decode_Array(model); - case GDScriptTypes.POOL_BYTE_ARRAY: - return this.decode_PoolByteArray(model); - case GDScriptTypes.POOL_INT_ARRAY: - return this.decode_PoolIntArray(model); - case GDScriptTypes.POOL_REAL_ARRAY: - return this.decode_PoolFloatArray(model); - case GDScriptTypes.POOL_STRING_ARRAY: - return this.decode_PoolStringArray(model); - case GDScriptTypes.POOL_VECTOR2_ARRAY: - return this.decode_PoolVector2Array(model); - case GDScriptTypes.POOL_VECTOR3_ARRAY: - return this.decode_PoolVector3Array(model); - case GDScriptTypes.POOL_COLOR_ARRAY: - return this.decode_PoolColorArray(model); - default: - return undefined; - } - } - - public encode_variant( - value: - | number - | bigint - | boolean - | string - | Map - | Array - | object - | undefined, - model?: BufferModel - ) { - if(typeof value === "number" && (value > 2147483647 || value < -2147483648)) { - value = BigInt(value); - } - - if (!model) { - let size = this.size_variant(value); - let buffer = Buffer.alloc(size + 4); - model = { - buffer: buffer, - offset: 0, - len: 0 - }; - this.encode_UInt32(size, model); - } - - switch (typeof value) { - case "number": - { - let is_integer = Number.isInteger(value); - if (is_integer) { - this.encode_UInt32(GDScriptTypes.INT, model); - this.encode_UInt32(value, model); - } else { - this.encode_UInt32(GDScriptTypes.REAL | (1 << 16), model); - this.encode_Float(value, model); - } - } - break; - case "bigint": - this.encode_UInt32(GDScriptTypes.INT | (1 << 16), 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 { - switch (value["__type__"]) { - case "Vector2": - this.encode_UInt32(GDScriptTypes.VECTOR2, model); - this.encode_Vector2(value, model); - break; - case "Rect2": - this.encode_UInt32(GDScriptTypes.RECT2, model); - this.encode_Rect2(value, model); - break; - case "Vector3": - this.encode_UInt32(GDScriptTypes.VECTOR3, model); - this.encode_Vector3(value, model); - break; - case "Transform2D": - this.encode_UInt32(GDScriptTypes.TRANSFORM2D, model); - this.encode_Transform2D(value, model); - break; - case "Plane": - this.encode_UInt32(GDScriptTypes.PLANE, model); - this.encode_Plane(value, model); - break; - case "Quat": - this.encode_UInt32(GDScriptTypes.QUAT, model); - this.encode_Quat(value, model); - break; - case "AABB": - this.encode_UInt32(GDScriptTypes.AABB, model); - this.encode_AABB(value, model); - break; - case "Basis": - this.encode_UInt32(GDScriptTypes.BASIS, model); - this.encode_Basis(value, model); - break; - case "Transform": - this.encode_UInt32(GDScriptTypes.TRANSFORM, model); - this.encode_Transform(value, model); - break; - case "Color": - this.encode_UInt32(GDScriptTypes.COLOR, model); - this.encode_Color(value, model); - break; - } - } - } - - return model.buffer; - } - - public get_buffer_dataset(buffer: Buffer, offset: number) { - let len = buffer.readUInt32LE(offset); - let model: BufferModel = { - buffer: buffer, - offset: offset + 4, - len: len - }; - - let output = []; - output.push(len + 4); - do { - let value = this.decode_variant(model); - output.push(value); - } while (model.len > 0); - - return output; - } - - private clean(value: number) { - return +Number.parseFloat(String(value)).toFixed(1); - } - - private decode_AABB(model: BufferModel) { - let px = this.decode_Float(model); - let py = this.decode_Float(model); - let pz = this.decode_Float(model); - let sx = this.decode_Float(model); - let sy = this.decode_Float(model); - let sz = this.decode_Float(model); - - return { - __type__: "AABB", - position: this.make_Vector3(px, py, pz), - size: this.make_Vector3(sx, sy, sz), - __render__: () => - `AABB (${this.clean(px)}, ${this.clean(py)}, ${this.clean( - pz - )} - ${this.clean(sx)}, ${this.clean(sy)}, ${this.clean(sz)})` - }; - } - - private decode_Array(model: BufferModel) { - let output: Array = []; - - let count = this.decode_UInt32(model); - - for (let i = 0; i < count; i++) { - let value = this.decode_variant(model); - output.push(value); - } - - return output; - } - - private decode_Basis(model: BufferModel) { - let x = this.decode_Vector3(model); - let y = this.decode_Vector3(model); - let z = this.decode_Vector3(model); - - return this.make_Basis( - [x.x, x.y, z.z as number], - [y.x, y.y, y.z as number], - [z.x, z.y, z.z as number] - ); - } - - private decode_Color(model: BufferModel) { - let r = this.decode_Float(model); - let g = this.decode_Float(model); - let b = this.decode_Float(model); - let a = this.decode_Float(model); - - return { - __type__: "Color", - r: r, - g: g, - b: b, - a: a, - __render__: () => - `Color (${this.clean(r)}, ${this.clean(g)}, ${this.clean( - b - )}, ${this.clean(a)})` - }; - } - - private decode_Dictionary(model: BufferModel) { - let output = new Map(); - - let count = this.decode_UInt32(model); - for (let i = 0; i < count; i++) { - let key = this.decode_variant(model); - let value = this.decode_variant(model); - output.set(key, value); - } - - return output; - } - - private decode_Double(model: BufferModel) { - let view = new DataView(model.buffer.buffer, model.offset, 8); - let d = view.getFloat64(0, true); - - model.offset += 8; - model.len -= 8; - - return d + 0.00000000001; - } - - private decode_Float(model: BufferModel) { - let view = new DataView(model.buffer.buffer, model.offset, 4); - let f = view.getFloat32(0, true); - - model.offset += 4; - model.len -= 4; - - return f + 0.00000000001; - } - - private decode_Int32(model: BufferModel) { - let u = model.buffer.readInt32LE(model.offset); - model.len -= 4; - model.offset += 4; - - return u; - } - - private decode_Int64(model: BufferModel) { - let view = new DataView(model.buffer.buffer, model.offset, 8); - let u = view.getBigInt64(0, true); - model.len -= 8; - model.offset += 8; - - return Number(u); - } - - private decode_NodePath(model: BufferModel) { - let 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; - 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[] = []; - for (let i = 0; i < total; i++) { - let str = this.decode_String(model); - if (i < name_count) { - names.push(str); - } else { - sub_names.push(str); - } - } - - return { - __type__: "NodePath", - path: names, - subpath: sub_names, - absolute: is_absolute, - __render__: () => `NodePath (${names.join(".")}:${sub_names.join(":")})` - }; - } - - private decode_Object(model: BufferModel) { - let class_name = this.decode_String(model); - let prop_count = this.decode_UInt32(model); - let props: { name: string; value: any }[] = []; - for (let i = 0; i < prop_count; i++) { - let name = this.decode_String(model); - let value = this.decode_variant(model); - props.push({ name: name, value: value }); - } - - return { __type__: class_name, properties: props }; - } - - private decode_Object_id(model: BufferModel) { - let id = this.decode_UInt64(model); - return { - __type__: "Object", - id: id, - __render__: () => `Object<${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); - - return { - __type__: "Plane", - x: x, - y: y, - z: z, - d: d, - __render__: () => - `Plane (${this.clean(x)}, ${this.clean(y)}, ${this.clean( - z - )}, ${this.clean(d)})` - }; - } - - private decode_PoolByteArray(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: number[] = []; - for (let i = 0; i < count; i++) { - output.push(model.buffer.readUInt8(model.offset)); - model.offset++; - model.len--; - } - - return output; - } - - private decode_PoolColorArray(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: { r: number; g: number; b: number; a: number }[] = []; - for (let i = 0; i < count; i++) { - output.push(this.decode_Color(model)); - } - - return output; - } - - private decode_PoolFloatArray(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: number[] = []; - for (let i = 0; i < count; i++) { - output.push(this.decode_Float(model)); - } - - return output; - } - - private decode_PoolIntArray(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: number[] = []; - for (let i = 0; i < count; i++) { - output.push(this.decode_Int32(model)); - } - - return output; - } - - private decode_PoolStringArray(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: string[] = []; - for (let i = 0; i < count; i++) { - output.push(this.decode_String(model)); - } - - return output; - } - - private decode_PoolVector2Array(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: { x: number; y: number }[] = []; - for (let i = 0; i < count; i++) { - output.push(this.decode_Vector2(model)); - } - - return output; - } - - private decode_PoolVector3Array(model: BufferModel) { - let count = this.decode_UInt32(model); - let output: { x: number; y: number; z: number | undefined }[] = []; - for (let i = 0; i < count; i++) { - output.push(this.decode_Vector3(model)); - } - - return output; - } - - 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); - - return { - __type__: "Quat", - x: x, - y: y, - z: z, - w: w, - __render__: () => - `Quat (${this.clean(x)}, ${this.clean(y)}, ${this.clean( - z - )}, ${this.clean(w)})` - }; - } - - private decode_Rect2(model: BufferModel) { - let x = this.decode_Float(model); - let y = this.decode_Float(model); - let sizeX = this.decode_Float(model); - let sizeY = this.decode_Float(model); - - return { - __type__: "Rect2", - position: this.make_Vector2(x, y), - size: this.make_Vector2(sizeX, sizeY), - __render__: () => - `Rect2 (${this.clean(x)}, ${this.clean(y)} - ${this.clean( - sizeX - )}, ${this.clean(sizeY)})` - }; - } - - private decode_String(model: BufferModel) { - let len = this.decode_UInt32(model); - let pad = 0; - if (len % 4 !== 0) { - pad = 4 - (len % 4); - } - - let str = model.buffer.toString("utf8", model.offset, model.offset + len); - len += pad; - - model.offset += len; - model.len -= len; - - return str; - } - - private decode_Transform(model: BufferModel) { - let b = this.decode_Basis(model); - let o = this.decode_Vector3(model); - - return { - __type__: "Transform", - basis: this.make_Basis( - [b.x.x, b.x.y, b.x.z as number], - [b.y.x, b.y.y, b.y.z as number], - [b.z.x, b.z.y, b.z.z as number] - ), - origin: this.make_Vector3(o.x, o.y, o.z), - __render__: () => - `Transform ((${this.clean(b.x.x)}, ${this.clean(b.x.y)}, ${this.clean( - b.x.z as number - )}), (${this.clean(b.y.x)}, ${this.clean(b.y.y)}, ${this.clean( - b.y.z as number - )}), (${this.clean(b.z.x)}, ${this.clean(b.z.y)}, ${this.clean( - b.z.z as number - )}) - (${this.clean(o.x)}, ${this.clean(o.y)}, ${this.clean( - o.z as number - )}))` - }; - } - - private decode_Transform2D(model: BufferModel) { - let origin = this.decode_Vector2(model); - let x = this.decode_Vector2(model); - let y = this.decode_Vector2(model); - - return { - __type__: "Transform2D", - origin: this.make_Vector2(origin.x, origin.y), - x: this.make_Vector2(x.x, x.y), - y: this.make_Vector2(y.x, y.y), - __render__: () => - `Transform2D ((${this.clean(origin.x)}, ${this.clean( - origin.y - )}) - (${this.clean(x.x)}, ${this.clean(x.y)}), (${this.clean( - y.x - )}, ${this.clean(y.x)}))` - }; - } - - 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 view = new DataView(model.buffer.buffer, model.offset, 8); - let u = view.getBigUint64(0, true); - model.len -= 8; - model.offset += 8; - - return Number(u); - } - - private decode_Vector2(model: BufferModel) { - let x = this.decode_Float(model); - let y = this.decode_Float(model); - - return this.make_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); - - return this.make_Vector3(x, y, z); - } - - private encode_AABB(value: object, model: BufferModel) { - this.encode_Vector3(value["position"], model); - this.encode_Vector3(value["size"], model); - } - - private encode_Array(arr: any[], model: BufferModel) { - let size = arr.length; - this.encode_UInt32(size, model); - arr.forEach(e => { - this.encode_variant(e, model); - }); - } - - private encode_Basis(value: object, 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: object, model: BufferModel) { - this.encode_Float(value["r"], model); - this.encode_Float(value["g"], model); - this.encode_Float(value["b"], model); - this.encode_Float(value["a"], model); - } - - private encode_Dictionary(dict: Map, model: BufferModel) { - let size = dict.size; - this.encode_UInt32(size, model); - let keys = Array.from(dict.keys()); - keys.forEach(key => { - let value = dict.get(key); - this.encode_variant(key, model); - this.encode_variant(value, model); - }); - } - - private encode_Float(value: number, model: BufferModel) { - let view = new DataView(model.buffer.buffer, model.offset); - view.setFloat32(0, value, true); - model.offset += 4; - } - - private encode_Double(value: number, model: BufferModel) { - let view = new DataView(model.buffer.buffer, model.offset); - view.setFloat64(0, value, true); - model.offset += 8; - } - - private encode_Plane(value: object, model: BufferModel) { - this.encode_Vector3(value["normal"], model); - this.encode_Float(value["d"], model); - } - - private encode_Quat(value: object, model: BufferModel) { - this.encode_Float(value["x"], model); - this.encode_Float(value["y"], model); - this.encode_Float(value["z"], model); - this.encode_Float(value["w"], model); - } - - private encode_Rect2(value: object, model: BufferModel) { - this.encode_Vector2(value["position"], model); - this.encode_Vector2(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_Transform(value: object, model: BufferModel) { - this.encode_Basis(value["basis"], model); - this.encode_Vector3(value["origin"], model); - } - - private encode_Transform2D(value: object, model: BufferModel) { - this.encode_Vector2(value["origin"], model); - this.encode_Vector2(value["x"], model); - this.encode_Vector2(value["y"], model); - } - - private encode_UInt32(int: number, model: BufferModel) { - model.buffer.writeUInt32LE(int, model.offset); - model.offset += 4; - } - - private encode_UInt64(value: bigint, model: BufferModel) { - let view = new DataView(model.buffer.buffer, model.offset, 8); - view.setBigUint64(0, value, true); - model.offset += 8; - } - - private encode_Vector2(value: any, model: BufferModel) { - this.encode_Float(value.x, model); - this.encode_Float(value.y, model); - } - - private encode_Vector3(value: any, model: BufferModel) { - this.encode_Float(value.x, model); - this.encode_Float(value.y, model); - this.encode_Float(value.z, model); - } - - private make_Basis(x: number[], y: number[], z: number[]) { - return { - __type__: "Basis", - x: this.make_Vector3(x[0], x[1], x[2]), - y: this.make_Vector3(y[0], y[1], y[2]), - z: this.make_Vector3(z[0], z[1], z[2]), - __render__: () => - `Basis ((${this.clean(x[0])}, ${this.clean(x[1])}, ${this.clean( - x[2] - )}), (${this.clean(y[0])}, ${this.clean(y[1])}, ${this.clean( - y[2] - )}), (${this.clean(z[0])}, ${this.clean(z[1])}, ${this.clean(z[2])}))` - }; - } - - private make_Vector2(x: number, y: number) { - return { - __type__: `Vector2`, - x: x, - y: y, - __render__: () => `Vector2 (${this.clean(x)}, ${this.clean(y)})` - }; - } - - private make_Vector3(x: number, y: number, z: number) { - return { - __type__: `Vector3`, - x: x, - y: y, - z: z, - __render__: () => - `Vector3 (${this.clean(x)}, ${this.clean(y)}, ${this.clean(z)})` - }; - } - - private size_Bool(): number { - return this.size_UInt32(); - } - - private size_Dictionary(dict: Map): number { - let size = this.size_UInt32(); - let keys = Array.from(dict.keys()); - keys.forEach(key => { - let 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[] - | 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: - if (Array.isArray(value)) { - size += this.size_array(value); - break; - } else if (value instanceof Map) { - size += this.size_Dictionary(value); - break; - } else { - switch (value["__type__"]) { - case "Vector2": - size += this.size_UInt32() * 2; - break; - case "Rect2": - size += this.size_UInt32() * 4; - break; - case "Vector3": - size += this.size_UInt32() * 3; - break; - case "Transform2D": - size += this.size_UInt32() * 6; - break; - case "Plane": - size += this.size_UInt32() * 4; - break; - case "Quat": - size += this.size_UInt32() * 4; - break; - case "AABB": - size += this.size_UInt32() * 6; - break; - case "Basis": - size += this.size_UInt32() * 9; - break; - case "Transform": - size += this.size_UInt32() * 12; - break; - case "Color": - size += this.size_UInt32() * 4; - break; - } - } - } - - return size; - } -}