;;; godot-rc.el --- Remote control Godot from within Emacs -*- lexical-binding: t -*- ;; This file is not part of GNU Emacs ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; This mode is intended to be used alongside the existing gdscript-modes, ;; and works to supplement them by introducing functionality such as scene ;; manipulation and editing. ;;; Code: (require 'magit-section) (defcustom godot-rc-host "127.0.0.1" "Host IP address for the Godot engine running godot-rc." :type 'string :group 'godot-rc) (defcustom godot-rc-port 6009 "Port for the Godot engine running godot-rc." :type 'number :group 'godot-rc) (defvar godot-rc-selected-scene nil "The currently selected scene - set automatically when visiting a .tscn file.") (defvar godot-rc-selected-node nil "The JSON representation of the selected node.") (defvar godot-rc-process nil "The network process for the remote control connection.") (defvar godot-rc-project-root nil "The project root of the active remote control connection.") (defvar godot-rc-scene-tree-buffer nil "The buffer that contains the scene tree.") (defun open-scene-tree-buffer () "Create and/or open the scene tree buffer." (setq godot-rc-scene-tree-buffer (get-buffer-create "*scene-tree*")) (with-current-buffer godot-rc-scene-tree-buffer (scene-tree-mode))) (defun godot-rc-process-filter-function (proc resp) "Handle RESP responses from the Godot PROC." (message (format "%s" proc)) (let ((response (json-parse-string resp :object-type 'plist :array-type 'list))) (cond ((equal "get-selected-node" (plist-get response :for-cmd)) (setq godot-rc-selected-node (plist-get response :value))) ((equal "get-scene-tree" (plist-get response :for-cmd)) (scene-tree-render (plist-get response :value)))))) (defun initialize-godot-rc () "Initialize godot-rc using the current buffer to find the Godot project." (interactive) (setq godot-rc-project-root (locate-dominating-file default-directory "project.godot")) (setq godot-rc-process (open-network-stream "godot-rc" "*godot-rc-stream*" godot-rc-host godot-rc-port)) (set-process-filter godot-rc-process #'godot-rc-process-filter-function) (open-scene-tree-buffer)) (defun kill-godot-rc () "Stop the godot-rc process and reset all variables to nil." (interactive) (delete-process godot-rc-process) (kill-buffer godot-rc-scene-tree-buffer) (setq godot-rc-selected-scene nil godot-rc-process nil godot-rc-project-root nil godot-rc-scene-tree-buffer nil)) (defun godot-rc-send-command (cmd-plist) "Send the CMD-PLIST as a JSON string to Godot." (when godot-rc-process (process-send-string godot-rc-process (json-serialize cmd-plist)))) ;; TODO: this needs to check if file-name is in the godot-rc-project-root and turn it into a res:// path (defun godot-rc-open-scene (file-name &optional quiet) "Open FILE-NAME as a scene in Godot." (when (not (equal file-name godot-rc-selected-scene)) (godot-rc-send-command `(:command "open-scene" :scene ,file-name :get-result ,(if quiet :false t))) (setq godot-rc-selected-scene file-name))) (defun tscn-check-visibility (&optional win) "When WIN changes to a TSCN, tell Godot to open the scene." (let ((buf (current-buffer))) (when (equal (file-name-extension (buffer-file-name buf)) "tscn") (godot-rc-open-scene (buffer-file-name buf))))) (add-hook 'find-file-hook #'tscn-check-visibility) (add-hook 'window-buffer-change-functions #'tscn-check-visibility) (add-hook 'window-selection-change-functions #'tscn-check-visibility) ;;; Scene-tree-mode and relevant commands (defclass scene-tree-node-property-section (magit-section) ((prop-name :initform nil)) "A `magit-section' used by `scene-tree-mode'") (defclass scene-tree-node-section (magit-section) ( ;(keymap :initform 'org-roam-node-map) (node :initform nil)) "A `magit-section' used by `scene-tree-mode'.") (defun insert-node (node depth) "Insert a NODE and it's children as magit sections." (magit-insert-section (scene-tree-node-section node) (magit-insert-heading (concat (make-string (* depth 2) ?\s) (propertize (plist-get node :name) 'face `(:inherit font-lock-function-call-face :underline ,(equal (plist-get node :path) (plist-get godot-rc-selected-node :path)))) " : " (propertize (plist-get node :type) 'face 'font-lock-type-face) "\n")) (dolist (n (plist-get node :children)) (insert-node n (+ 1 depth))))) (defun insert-node-property-category (category) "Insert a node CATEGORY as a magit section." (magit-insert-section (magit-section) (magit-insert-heading (propertize (plist-get category :name) 'face 'font-lock-constant-face)) (dolist (n (plist-get category :props)) (insert-node-property n)))) (defun insert-node-property (property) "Insert a node PROPERTY as a magit section." (magit-insert-section (scene-tree-node-property-section (plist-get property :name)) (magit-insert-heading (concat " " (propertize (plist-get property :name) 'face 'font-lock-variable-use-face) " = " (format "%s" (plist-get property :value)))))) (defun scene-tree-render (tree) "Render the scene tree." (with-current-buffer godot-rc-scene-tree-buffer (let ((l (line-number-at-pos)) (inhibit-read-only t)) (erase-buffer) (magit-insert-section (magit-section "SceneTree") (magit-insert-heading (file-name-nondirectory godot-rc-selected-scene) "\n\n") (insert-node tree 0)) (insert "\n\n") (magit-insert-section (magit-section "NodeProperties") (magit-insert-heading (format "%s | Properties\n" (plist-get godot-rc-selected-node :name))) (dolist (n (plist-get godot-rc-selected-node :props)) (insert-node-property-category n))) (goto-line l)))) (defun scene-tree-update-property-at-point (new-value) "Update the property at point to be NEW-VALUE." (interactive "xNew value: ") (godot-rc-send-command `(:command "update-property" :path ,(plist-get godot-rc-selected-node :path) :name ,(slot-value (magit-current-section) 'value) :new-value ,new-value))) (defun scene-tree-interact-at-point () "Rename node at point." (interactive) (cond ((scene-tree-node-section-p (magit-current-section)) (let ((node-path (plist-get (slot-value (magit-current-section) 'value) :path))) (godot-rc-send-command `(:command "select-node" :path ,node-path)))) ((scene-tree-node-property-section-p (magit-current-section)) (call-interactively 'scene-tree-update-property-at-point)))) (defvar scene-tree-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map magit-section-mode-map) (define-key map [return] 'scene-tree-interact-at-point) map) "Keymap for scene-tree-mode.") (define-derived-mode scene-tree-mode magit-section-mode "Scene Tree" "Mode for showing the scene tree." :group 'godot-rc) (provide 'godot-rc) ;;; godot-rc.el ends here