201 lines
7.5 KiB
EmacsLisp
201 lines
7.5 KiB
EmacsLisp
;;; 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 <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
;;; 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
|