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

* Significantly rework the debugger to add Godot 4 support.

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

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

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

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

---------

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

55
.eslintrc.json Normal file
View File

@@ -0,0 +1,55 @@
{
"env": {
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"ignorePatterns": [
"out/**",
"node_modules/**",
"prism.js"
],
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"@typescript-eslint/tslint"
],
"rules": {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/ban-types": "warn",
"no-async-promise-executor": "warn",
"no-inner-declarations": "warn",
"no-prototype-builtins": "warn",
"no-constant-condition": "warn",
"prefer-const": "warn",
"no-useless-escape": "off",
"no-var": "off",
"indent": [
"off",
"tab"
],
"linebreak-style": [
"off",
"windows"
],
"quotes": [
"warn",
"double"
],
"semi": [
"error",
"always"
]
}
}

View File

@@ -3,12 +3,6 @@
A complete set of tools to code games with
[Godot Engine](http://www.godotengine.org/) in Visual Studio Code.
> **Warning**
>
> This plugin requires manual configuration to work with Godot 4!
> See the [`gdscript_lsp_server_port` setting](#gdscript_lsp_server_port)
> item under the Configuration section below.
**IMPORTANT NOTE:** Versions 1.0.0 and later of this extension only support
Godot 3.2 or later.
@@ -19,10 +13,13 @@ experience as comfortable as possible:
- Syntax highlighting for the GDScript (`.gd`) language
- Syntax highlighting for the `.tscn` and `.tres` scene formats
- Syntax highlighting for the `.gdshader` shader format
- Full typed GDScript support
- Optional "Smart Mode" to improve productivity with dynamically typed scripts
- Function definitions and documentation display on hover (see image below)
- Rich autocompletion
- Switch from a `.gd` file to the related `.tscn` file (default keybind is `alt+o`)
- In-editor Scene Preview
- Display script warnings and errors
- Ctrl + click on a variable or method call to jump to its definition
- Full documentation of the Godot Engine's API supported (select *Godot Tools: List native classes of Godot* in the Command Palette)
@@ -73,18 +70,14 @@ for Godot by following these steps:
You can use the following settings to configure Godot Tools:
##### `editor_path`
- `godotTools.editorPath.godot3`
- `godotTools.editorPath.godot4`
The absolute path to the Godot editor executable. _Under Mac OS, this is the executable inside of Godot.app._
The path to the Godot editor executable. _Under Mac OS, this is the executable inside of Godot.app._
##### `gdscript_lsp_server_port`
- `godotTools.lsp.headless`
The WebSocket server port of the GDScript language server.
For Godot 3, the default value of `6008` should work out of the box.
**For Godot 4, this value must be changed to `6005` for this extension to connect to the language server.**
See [this tracking issue](https://github.com/godotengine/godot-vscode-plugin/issues/473) for more information.
When using Godot >3.6 or >4.2, Headless LSP mode is available. In Headless mode, the extension will attempt to launch a windowless instance of the Godot editor to use as its Language Server.
#### GDScript Debugger
@@ -102,19 +95,24 @@ To configure the GDScript debugger:
5. Change any relevant settings.
6. Press F5 to launch.
*Configurations*
### *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`.
None: seriously. This is valid debugging configuration:
```json
{ "name": "Launch", "type": "godot" }
```
_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.
`project`: Absolute path to a directory with a project.godot file. Defaults to the currently open VSCode workspace with `${workspaceFolder}`.
`port`: The port number for the Godot remote debugger to use.
`address`: The IP address for the Godot remote debugger to use.
`scene_file`: Path to a scene file to run instead of the projects 'main scene'.
`editor_path`: Absolute path to the Godot executable to be used for this debug profile.
`additional_options`: Additional command line arguments.
*Usage*

4141
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
"main": "./out/extension.js",
"scripts": {
"compile": "tsc -p ./",
"lint": "tslint -p ./",
"lint": "eslint ./src --quiet",
"watch": "tsc -watch -p ./",
"package": "vsce package",
"vscode:prepublish": "npm run esbuild-base -- --minify",
@@ -43,34 +43,32 @@
"contributes": {
"commands": [
{
"category": "Godot Tools",
"command": "godotTools.openEditor",
"title": "Godot Tools: Open workspace with Godot editor"
"title": "Open workspace with Godot editor"
},
{
"category": "Godot Tools",
"command": "godotTools.startLanguageServer",
"title": "Godot Tools: Start the GDScript Language Server for this workspace"
"title": "Start the GDScript Language Server for this workspace"
},
{
"category": "Godot Tools",
"command": "godotTools.stopLanguageServer",
"title": "Godot Tools: Stop the GDScript Language Server for this workspace"
},
{
"command": "godotTools.runProject",
"title": "Godot Tools: Run workspace as Godot project"
},
{
"command": "godotTools.runProjectDebug",
"title": "Godot Tools: Run workspace as Godot project with visible collision shapes and navigation meshes"
"title": "Stop the GDScript Language Server for this workspace"
},
{
"category": "Godot Tools",
"command": "godotTools.listNativeClasses",
"title": "Godot Tools: List native classes of godot"
"title": "List native classes of godot"
},
{
"category": "Godot Tools",
"command": "godotTools.openTypeDocumentation",
"title": "Godot Tools: Open Type Documentation"
"title": "Open Type Documentation"
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.inspectNode",
"title": "Inspect Remote Node",
"icon": {
@@ -79,6 +77,7 @@
}
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.refreshSceneTree",
"title": "Refresh",
"icon": {
@@ -87,6 +86,7 @@
}
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.refreshInspector",
"title": "Refresh",
"icon": {
@@ -95,6 +95,7 @@
}
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.editValue",
"title": "Edit value",
"icon": {
@@ -103,54 +104,85 @@
}
},
{
"command": "godotTools.scenePreview.refresh",
"title": "Godot Tools: Refresh Scene Preview"
"category": "Godot Tools",
"command": "godotTools.debugger.debugCurrentFile",
"title": "Debug Current File",
"icon": "$(play)"
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.debugPinnedFile",
"title": "Debug Pinned File",
"icon": "$(play)"
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.pinFile",
"title": "Pin Scene File",
"icon": "resources/pin_off.svg"
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.unpinFile",
"title": "Unpin Scene File",
"icon": "resources/pin_on.svg"
},
{
"category": "Godot Tools",
"command": "godotTools.debugger.openPinnedFile",
"title": "Open the currently pinned scene"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.refresh",
"title": "Refresh Scene Preview"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.pin",
"title": "Pin Scene Preview",
"icon": "resources/pin_off.svg"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.unpin",
"title": "Unpin Scene Preview",
"icon": "resources/pin_on.svg"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.goToDefinition",
"title": "Go to Definition"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.copyNodePath",
"title": "Copy Node Path"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.copyResourcePath",
"title": "Copy Resource Path"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.openScene",
"title": "Open Scene"
},
{
"category": "Godot Tools",
"command": "godotTools.scenePreview.openScript",
"title": "Open Script"
},
{
"category": "Godot Tools",
"command": "godotTools.switchSceneScript",
"title": "Godot Tools: Switch Scene/Script"
},
{
"command": "godotTools.setSceneFile",
"title": "Set as Scene File"
},
{
"command": "godotTools.copyResourcePathContext",
"title": "Copy Resource Path"
"title": "Switch Scene/Script"
},
{
"category": "Godot Tools",
"command": "godotTools.copyResourcePath",
"title": "Godot Tools: Copy Resource Path"
"title": "Copy Resource Path"
}
],
"keybindings": [
@@ -164,6 +196,16 @@
"type": "object",
"title": "Godot Tools",
"properties": {
"godotTools.editorPath.godot3": {
"type": "string",
"default": "godot3",
"description": "The absolute path to the Godot 3 editor executable"
},
"godotTools.editorPath.godot4": {
"type": "string",
"default": "godot4",
"description": "The absolute path to the Godot 4 editor executable"
},
"godotTools.lsp.serverProtocol": {
"type": [
"string"
@@ -194,21 +236,6 @@
"default": false,
"description": "Whether to launch the LSP as a headless child process"
},
"godotTools.editorPath.godot3": {
"type": "string",
"default": "godot3",
"description": "The absolute path to the Godot 3 editor executable"
},
"godotTools.editorPath.godot4": {
"type": "string",
"default": "godot4",
"description": "The absolute path to the Godot 4 editor executable"
},
"godotTools.sceneFileConfig": {
"type": "string",
"default": "",
"description": "The scene file to run"
},
"godotTools.lsp.autoReconnect.enabled": {
"type": "boolean",
"default": true,
@@ -224,27 +251,27 @@
"default": 10,
"description": "How many times the client will attempt to reconnect"
},
"godotTools.forceVisibleCollisionShapes": {
"godotTools.debugger.forceVisibleCollisionShapes": {
"type": "boolean",
"default": false,
"description": "Force the project to run with visible collision shapes"
},
"godotTools.forceVisibleNavMesh": {
"godotTools.debugger.forceVisibleNavMesh": {
"type": "boolean",
"default": false,
"description": "Force the project to run with visible navigation meshes"
},
"godotTools.nativeSymbolPlacement": {
"godotTools.documentation.newTabPlacement": {
"enum": [
"active",
"beside"
],
"enumDescriptions": [
"Place the native symbol window in the active tabs group",
"Place the native symbol window beside the active tabs group"
"Place new documentation views in the active tabs group",
"Place new documentation views beside the active tabs group"
],
"default": "beside",
"description": "Where to place the native symbol windows"
"description": "Where to place new documentation views"
},
"godotTools.scenePreview.previewRelatedScenes": {
"enum": [
@@ -274,6 +301,17 @@
],
"configuration": "./configurations/gdscript-configuration.json"
},
{
"id": "gdscene",
"aliases": [
"GDScene",
"gdscene"
],
"extensions": [
"tscn"
],
"configuration": "./configurations/gdresource-configuration.json"
},
{
"id": "gdresource",
"aliases": [
@@ -283,7 +321,6 @@
"extensions": [
"godot",
"tres",
"tscn",
"import",
"gdns",
"gdnlib"
@@ -328,64 +365,74 @@
{
"type": "godot",
"label": "GDScript Godot Debug",
"program": "./out/debugger/debug_adapter.js",
"runtime": "node",
"configurationAttributes": {
"launch": {
"required": [
"project",
"port",
"address"
],
"required": [],
"properties": {
"project": {
"type": "string",
"description": "Absolute path to a directory with a project.godot file.",
"default": "${workspaceFolder}"
},
"port": {
"type": "number",
"description": "The port number for the Godot remote debugger to use.",
"default": 6007
},
"address": {
"type": "string",
"description": "The IP address for the Godot remote debugger to use.",
"default": "127.0.0.1"
},
"launch_game_instance": {
"type": "boolean",
"description": "Whether to launch an instance of the workspace's game, or wait for a debug session to connect.",
"default": true
"port": {
"type": "number",
"description": "The port number for the Godot remote debugger to use.",
"default": 6007
},
"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": {
"scene": {
"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.",
"enum": [
"main",
"current",
"pinned"
],
"enumDescriptions": [
"Launch the 'main_scene' specified in project.godot",
"Launch the scene (or related scene) in the current editor",
"Launch the pinned scene"
],
"description": "Scene file to run when debugging. Choices are 'main', 'current', 'pinned', or providing a custom path to a scene.",
"default": ""
},
"editor_path": {
"type": "string",
"description": "Absolute path to the Godot executable to be used for this debug profile."
},
"additional_options": {
"type": "string",
"description": "Additional command line arguments.",
"default": ""
}
}
},
"attach": {
"required": [],
"properties": {
"address": {
"type": "string",
"description": "The IP address for the Godot remote debugger to use.",
"default": "127.0.0.1"
},
"port": {
"type": "number",
"description": "The port number for the Godot remote debugger to use.",
"default": 6007
}
}
}
},
"initialConfigurations": [
{
"name": "GDScript Godot",
"name": "GDScript: Launch Godot",
"type": "godot",
"request": "launch",
"project": "${workspaceFolder}",
"port": 6007,
"address": "127.0.0.1",
"launch_game_instance": true,
"launch_scene": false,
"additional_options": ""
}
],
@@ -394,15 +441,47 @@
"label": "GDScript Godot Debug: Launch",
"description": "A new configuration for debugging a Godot project.",
"body": {
"name": "GDScript: Launch Project",
"type": "godot",
"request": "launch",
"project": "${workspaceFolder}",
"port": 6007,
"address": "127.0.0.1",
"launch_game_instance": true,
"launch_scene": false,
"additional_options": ""
}
},
{
"label": "GDScript: Launch Current File",
"description": "A new configuration for debugging a Godot project.",
"body": {
"name": "GDScript: Launch Current File",
"type": "godot",
"request": "launch",
"scene": "current",
"project": "${workspaceFolder}",
"additional_options": ""
}
},
{
"label": "GDScript: Launch Pinned File",
"description": "A new configuration for debugging a Godot project.",
"body": {
"name": "GDScript: Launch Pinned File",
"type": "godot",
"request": "launch",
"scene": "pinned",
"project": "${workspaceFolder}",
"additional_options": ""
}
},
{
"label": "GDScript Godot Debug: Attach",
"description": "A new configuration for debugging a Godot project.",
"body": {
"name": "GDScript: Attach to Godot",
"type": "godot",
"request": "attach",
"address": "127.0.0.1",
"port": 6007
}
}
]
}
@@ -424,14 +503,12 @@
"views": {
"debug": [
{
"id": "active-scene-tree",
"name": "Active Scene Tree",
"when": "inDebugMode && debugType == 'godot'"
"id": "activeSceneTree",
"name": "Active Scene Tree"
},
{
"id": "inspect-node",
"name": "Inspector",
"when": "inDebugMode && debugType == 'godot'"
"id": "inspectNode",
"name": "Inspector"
}
],
"godotTools": [
@@ -441,6 +518,20 @@
}
]
},
"viewsWelcome": [
{
"view": "activeSceneTree",
"contents": "Scene Tree data has not been requested"
},
{
"view": "inspectNode",
"contents": "Node has not been inspected"
},
{
"view": "scenePreview",
"contents": "Open a Scene to see a preview of its structure"
}
],
"menus": {
"commandPalette": [
{
@@ -476,46 +567,58 @@
"when": "false"
},
{
"command": "godotTools.copyResourcePathContext",
"command": "godotTools.debugger.editValue",
"when": "false"
},
{
"command": "godotTools.debugger.inspectNode",
"when": "false"
},
{
"command": "godotTools.debugger.refreshSceneTree",
"when": "false"
},
{
"command": "godotTools.debugger.refreshInspector",
"when": "false"
}
],
"view/title": [
{
"command": "godotTools.debugger.refreshSceneTree",
"when": "view == active-scene-tree",
"when": "view == activeSceneTree",
"group": "navigation"
},
{
"command": "godotTools.debugger.refreshInspector",
"when": "view == inspect-node",
"when": "view == inspectNode",
"group": "navigation"
},
{
"command": "godotTools.scenePreview.pin",
"when": "view == scenePreview && !godotTools.context.scenePreviewPinned",
"when": "view == scenePreview && !godotTools.context.scenePreview.pinned",
"group": "navigation"
},
{
"command": "godotTools.scenePreview.unpin",
"when": "view == scenePreview && godotTools.context.scenePreviewPinned",
"when": "view == scenePreview && godotTools.context.scenePreview.pinned",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "godotTools.debugger.inspectNode",
"when": "view == active-scene-tree",
"when": "view == activeSceneTree",
"group": "inline"
},
{
"command": "godotTools.debugger.inspectNode",
"when": "view == inspect-node && viewItem == remote_object",
"when": "view == inspectNode && viewItem == remote_object",
"group": "inline"
},
{
"command": "godotTools.debugger.editValue",
"when": "view == inspect-node && viewItem == editable_value",
"when": "view == inspectNode && viewItem == editable_value",
"group": "inline"
},
{
@@ -544,18 +647,58 @@
],
"explorer/context": [
{
"command": "godotTools.setSceneFile",
"group": "2_workspace"
"command": "godotTools.debugger.pinFile",
"group": "2_workspace",
"when": "resourceLangId in godotTools.context.sceneLikeFiles && !(resourcePath in godotTools.context.pinnedScene)"
},
{
"command": "godotTools.copyResourcePathContext",
"command": "godotTools.debugger.unpinFile",
"group": "2_workspace",
"when": "resourceLangId in godotTools.context.sceneLikeFiles && (resourcePath in godotTools.context.pinnedScene)"
},
{
"command": "godotTools.copyResourcePath",
"group": "6_copypath"
}
],
"editor/title/run": [
{
"command": "godotTools.debugger.debugCurrentFile",
"group": "navigation@10",
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace"
},
{
"command": "godotTools.debugger.debugPinnedFile",
"group": "navigation@10",
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace"
}
],
"editor/title": [
{
"command": "godotTools.debugger.pinFile",
"group": "navigation@11",
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace && !(resourcePath in godotTools.context.pinnedScene)"
},
{
"command": "godotTools.debugger.unpinFile",
"group": "navigation@11",
"when": "editorLangId in godotTools.context.sceneLikeFiles && !isInDiffEditor && !virtualWorkspace && (resourcePath in godotTools.context.pinnedScene)"
}
],
"editor/title/context": [
{
"command": "godotTools.copyResourcePathContext",
"command": "godotTools.copyResourcePath",
"group": "1_godot"
},
{
"command": "godotTools.debugger.pinFile",
"group": "1_godot",
"when": "resourceLangId in godotTools.context.sceneLikeFiles && !(resourcePath in godotTools.context.pinnedScene)"
},
{
"command": "godotTools.debugger.unpinFile",
"group": "1_godot",
"when": "resourceLangId in godotTools.context.sceneLikeFiles && (resourcePath in godotTools.context.pinnedScene)"
}
],
"editor/context": [
@@ -566,34 +709,40 @@
},
{
"command": "godotTools.switchSceneScript",
"when": "editorLangId == 'gdscript' || editorLangId == 'gdresource'",
"when": "editorLangId in godotTools.context.sceneLikeFiles",
"group": "custom1@1"
}
]
}
},
"devDependencies": {
"@types/marked": "^0.6.5",
"@types/marked": "^4.0.8",
"@types/mocha": "^9.1.0",
"@types/node": "^18.15.0",
"@types/prismjs": "^1.16.8",
"@types/vscode": "^1.80.0",
"@types/ws": "^8.2.2",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/eslint-plugin-tslint": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vscode/vsce": "^2.21.0",
"esbuild": "^0.15.2",
"esbuild": "^0.17.15",
"eslint": "^8.37.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"tslint": "^5.20.1",
"typescript": "^5.2.2"
},
"dependencies": {
"@vscode/debugadapter": "^1.64.0",
"@vscode/debugprotocol": "^1.64.0",
"await-notify": "^1.0.1",
"global": "^4.4.0",
"marked": "^4.0.11",
"net": "^1.0.2",
"prismjs": "^1.17.1",
"terminate": "^2.5.0",
"vscode-debugadapter": "^1.38.0",
"vscode-languageclient": "^7.0.0",
"ws": "^8.4.2"
"ws": "^8.13.0"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -86,7 +86,7 @@ function get_class_list(modules) {
}
function discover_modules() {
const modules = []
const modules = [];
// a valid module is a subdir of modulesPath, and contains a subdir 'icons'
fs.readdirSync(modulesPath, {withFileTypes:true}).forEach(mod => {
@@ -106,7 +106,7 @@ function get_icons() {
const modules = discover_modules();
const classes = get_class_list(modules);
const searchPaths = [iconsPath]
const searchPaths = [iconsPath];
modules.forEach(mod => {
searchPaths.push(join(mod, 'icons'));
});

View File

@@ -4,7 +4,7 @@
"target": "es2020",
"outDir": "out",
"lib": [
"es2020",
"es2022",
"dom"
],
"sourceMap": true,