
2025/12/1, 0:00
本記事は Emacs Advent Calendar 2025 の初日の記事です
この記事はQiitaのEmacs好きなIT技術者、インディーゲーム開発者(特にRPGツクールMZを使っているツクラー)向けに発信しております。
こんにちは! デジタルゲーム/サウンド創作サークルのサークル主、たちやまと申します。
趣味の一環でRPGツクールMZを使って謎解きアドベンチャーゲームを開発しています。
謎解きの実装やグラフィックの処理など、メインのゲーム開発はMZ上で行うのですが、キャラクターの台詞や、立ち絵の処理のスクリプトなどの執筆については、テキストエディタであるEmacsを使用しています。
執筆に当たって使用しているメジャーモードは、私が自作したmz-scripter-mode1)です。
Emacsは学生時分から10年以上2)使っていますが、メジャーモードは初めて自作しました。
Lispの基本的な部分は書けるので、「やさしいEmacs-Lisp講座」3)でメジャーモード開発周りの知識を仕入れてから、ライブラリ関数などの簡単な調べものはcopilotを併用しつつ開発しました。
Emacs好きな皆さんへ
私が初めて開発したメジャーモードの紹介記事です。
elisp的に新しいもの、目立つものは無いと思います。4)
「初心者がなんか作っとんな」という生暖かい目でお読みください
インディーゲーム開発界隈の皆さんへ
アドベンチャーパートが有るゲームであれば、シナリオを書く系の作業はどのゲームエンジン5)を使用していても手間が掛かる部分だと思います。
私はRPGツクールMZが大好き6)ですが、アドベンチャーパートの開発に多大な時間が掛かっていました。
MZの会話ウィンドウは一般的なテキスト入力のUIなのですが、私のゲーム開発がテキストと同時に各種演出をたくさん入力するものであったためです。
そこで、テキストを入力するためのソフトウェアであるテキストエディタEmacsでシナリオ執筆用の専用モードを開発してみた、という記事です。
技術的な部分は中々伝わりづらいと思いますが、ゲーム開発効率化の何かのヒントになれば幸いです。
2ファイルで構成しています。github でも公開中
;; mz-scripter-mode.el ;; MZ Scripter mode, RPGツクールMZの台本執筆モード ;; 台本の執筆機能、および台本からMZのJSON形式への変換機能。 ;; 命名規則 ;; interactiveな関数はmz-scripter-* ;; 内部関数はMZScr/* ;; 内部変数はmzscr-* ;;;;;;;; ライブラリ (require 'cl-lib) ; common-lisp関数が収録されたライブラリ (require 'posframe) ; ポップアップで文字列表示 (require 'mz-scripter-mode-highlight) ; ハイライト ;;;;;;;; Keymap (defvar mz-scripter-mode-keymap (let ((keymap (make-sparse-keymap))) ;; (define-key keymap (kbd "C-c c") 'よくつかう機能のために残しとく) (define-key keymap (kbd "C-c t") 'MZScr/test) ; 開発用 (define-key keymap (kbd "C-c l") 'MZScr/list-switches) ; 効果音、演出番号を一覧表示 (define-key keymap (kbd "C-c h") 'describe-bindings) ; ヘルプ表示(Emacs標準の機能を呼ぶ) (define-key keymap (kbd "C-c i") 'MZScr/insert-direction) ; 演出(効果音、立ち絵)の挿入 (define-key keymap (kbd "C-c r") 'mz-scripter-mode-exe-region) ; リージョンで選択された台本をJSONに変換 ;;(define-key keymap (kbd "C-c b") 'mz-scripter-mode-exe-buffer) ; バッファの台本をJSONに変換 (define-key keymap (kbd "C-c !") 'mz-scripter-mode-export-to-mz-buffer) ; バッファの台本をJSONに変換 keymap) "Keymap for MZ Scripter mode") ;;;;;;;; 共通変数 (defgroup mz-scripter-mode nil "A major mode for Scripting MZ scenario." :group 'convenience) (defcustom mzscr-json-exportfile-path "C://workspace//rpgMaker//games//MZ//bknk2_v1.3.0//data//Map210.json" "json file path to export." :type 'string :group 'mz-scripter-mode) (defcustom mzscr-json-template-path "~/.emacs.d/elisp/mz-scripter-mode/Map210_template.json" "json template file using while exporting." :type 'string :group 'mz-scripter-mode) (defface mzscr-header-line-face '((t (:background "#FFFEE6" :foreground "dark slate gray" :weight bold))) "Face for the header line.") (defcustom mzscr-next-event-title "東京ゲームダンジョン10" "event title for display on right up of buffer." :type 'string :group 'mz-scripter-mode) (defcustom mzscr-next-event-date "2025-12-31" "date of next event, format is %Y-%m-%d, ex) 1970-01-01" :type 'string :group 'mz-scripter-mode) ;; マップJSONファイルにエクスポートする際に使用するファイルのパス ;; (setq mzscr-json-exportfile-path "C://workspace//rpgMaker//games//MZ//bknk2_v1.0.0//data//Map210.json") ;; (setq mzscr-json-template-path "~/.emacs.d/elisp/mz-scripter-mode/Map210_template.json") ;; キャラ立ち絵pic, (キャラ名 picファイル名) (setq mzscr-pic-name-alist '(("tc" "たま" "tc.png") ("th" "珠" "th.png") ("yc" "やにゅし" "yc.png") ("yh" "やぬし" "yh.png") ("wc" "ウィリーキャット" "wc.png") ("wh" "ウィリー" "wh.png") ("yc" "やにゅし" "yc.png") ("yh" "やぬし" "yh.png") ("ah" "葵" "yh.png") ("ac" "あおい" "ac.png"))) ;; キャラ辞書 key:キャラ略称、value:キャラ名とキャラ文字色 (setq mzscr-character-info-alist '(("txt" "" "") ; 名無しとか地の文用 ("tc" "たま" "\\\\c[23]")("th" "珠" "\\\\c[23]")("yc" "やにゅし" "\\\\c[2]")("yh" "やぬし" "\\\\c[2]") ("wc" "ウィリーキャット" "\\\\c[17]")("wh" "ウィリー" "\\\\c[17]")("hachi" "はちわれ" "\\\\c[21]")("kuro" "くろ" "\\\\c[21]") ("ac" "あおい" "\\\\c[13]")("ah" "葵" "\\\\c[13]")("k" "神主" "\\\\c[5]")("w" "わさポン" "")("iseki" "遺跡ネッコ" "") ("iseki_fu1" "iseki_fu1" "\\\\c[7]")("iseki_fu2" "iseki_fu2" "\\\\c[7]")("iseki_fu3" "iseki_fu3" "\\\\c[7]") ("konoe1" "近衛兵ネッコ1" "")("konoe2" "近衛兵ネッコ2" "")("king" "おうさまネッコ" "")("jiiya" "じいや" "")("master" "マスター" ""))) ;; (assoc "ah" character-info-alist) ;assoc関数でkey検索可能 ;; スイッチ一覧 SE, アクション別にまとめて定義 (setq mzscr-switches '((Sound (("ツッコミ" 42) ("シャキーン" 43) ("ボヨン" 44) ("爆発" 45) ("和太鼓" 46) ("ドンドンパフパフ" 47) ("指パッチン" 48) ("ファンファーレ" 49) ("時代劇" 50) ("ウワー" 51) ("スポッ1" 52)("スポッ2" 53) ("拍子木1" 54) ("拍子木2" 55) ("レジスター" 56) ("将棋" 57) ("かわいい音" 58) ("ブン!" 59) ("チーン" 60)("ぱっ" 61))) (Action (("富竹フラッシュ" 85))))) (defun MZScr/test () "for development" (interactive)) ;;;;;;;; メジャーモード定義 ;; hookの定義 慣習的にnilで初期化 (defvar mz-scripter-mode-hook nil "Hook run after `mz-scripter-mode` is enabled.") (defun mz-scripter-mode () "RPGMakerMZ Scripter mode" (interactive) (use-local-map mz-scripter-mode-keymap) ; MZ Scripterモード用キーマップを使用 (setq major-mode 'mz-scripter-mode ; メジャーモードの設定 mode-name "MZ Scripter") ; モードライン上のモード名のフィールド (mz-scripter-mode-highlight) (face-remap-add-relative 'default :background "#FFFEE6") ; ハイライトを有効化 (setq-local ; バッファ右上に次のイベントまでの残り日数を表示 header-line-format '(:eval (MZScr/get-header-text)))) ;; posfrmeで右上に残りの日時を表示するコードだが、動作が微妙なのでオミット ;; (let ((target-window (get-buffer-window (current-buffer)))) ;; (if (and target-window (window-live-p target-window)) ;; (posframe-show " *header-posframe*" ;; :string (MZScr/get-header-text) ;; :poshandler #'posframe-poshandler-frame-top-left-corner ;; :background-color "#333333" ;; :foreground-color "white" ;; :internal-border-width 1 ;; :timeout nil)))) (defun MZScr/get-header-text () (let* ((text (concat mzscr-next-event-title "(" mzscr-next-event-date "), 残り" (number-to-string (MZScr/days-until mzscr-next-event-date)) "日!")) (text-width (string-width text)) (win-width (window-width)) (padding (max 0 (- win-width text-width))) (padded-text (concat (make-string padding ?\s) text))) (propertize padded-text 'face 'mzscr-header-line-face))) (defun MZScr/days-until (date-string) "Calculate the number of days from today until DATE-STRING (format: YYYY-MM-DD)." (let* ((parts (mapcar #'string-to-number (split-string date-string "-"))) (year (nth 0 parts)) (month (nth 1 parts)) (day (nth 2 parts)) (target-time (encode-time 0 0 0 day month year)) (now (current-time)) (diff (time-subtract target-time now)) (days (floor (/ (float-time diff) 86400)))) days)) ;;;;;;;; エディタ機能 (defun MZScr/list-switches () "キャラ辞書とスイッチ一覧を横に並べて専用バッファに表示する。" (interactive) (with-output-to-temp-buffer "*キャラとスイッチ一覧*" (princ (format "%-20s %-10s %-10s | %-20s %-5s\n" "名前" "略称" "色コード" "スイッチ名" "ID")) (princ (make-string 80 ?-)) ;; 最大行数を取得(長い方に合わせる) (let* ((char-list mzscr-character-info-alist) (switch-list (apply #'append (mapcar #'cadr mzscr-switches))) (max-len (max (length char-list) (length switch-list)))) (dotimes (i max-len) (let* ((char-entry (nth i char-list)) (sw-entry (nth i switch-list)) (char-key (nth 0 char-entry)) (char-name (nth 1 char-entry)) (char-color (nth 2 char-entry)) (sw-name (if sw-entry (car sw-entry) "")) (sw-id (if sw-entry (cadr sw-entry) ""))) (princ (format "%-20s %-10s %-10s | %-20s %-5s\n" (or char-name "") (or char-key "") (or char-color "") sw-name sw-id)) ))) (princ "\n") (princ "mzscrファイルの記法\n") (princ "tb, 文字拡大(Text Big) → \\{\n") (princ "ts, 文字縮小(Text Small) → \\}\n") (princ "ws, 1/4s待ち(Wait Short) → \\.\n") (princ "wl, 1s待ち(Wait Long) → \\|\n") (princ "te, リターン待ち(Text End) → \\!\n") (princ "fnn, ネコフォント → \\fn[n]\n") (princ "fn, ギャグフォント → \\fn[g]\n") (princ "fne, フォント変更終了 → \\fn\n"))) (defun MZScr/insert-direction () "演出(効果音, 立ち絵, 演出)を挿入する" (interactive) (let ((choice (read-char-choice "演出の種類を選択 効果音[s] | 立ち絵[p] | 演出[d] :" '("s" "p" "d")))) (cond ((char-equal choice ?s) ; ?sでsの文字コード (MZScr/insert-sound-effect)) ; 効果音の挿入処理 ((char-equal choice ?p) ; 立ち絵の挿入処理 (MZScr/insert-standing-picture)) ((char-equal choice ?d) ; 演出の挿入処理 (MZScr/insert-staging))))) (defun MZScr/insert-sound-effect () "効果音を挿入する" ;; スイッチのリストから効果音の一覧バッファを作る ;; (("ツッコミ" 42) ("シャキーン" 43)) (let ((se-switch-list (cadr (assoc 'Sound mzscr-switches)))) ; 効果音スイッチ一覧 (let ((se-option-list (mapcar (lambda (e) (format "%s" e)) (number-sequence 0 (1- (length se-switch-list)))))) ; 選択肢用のオプション (let ((choice) (config (current-window-configuration))) ;(save-selected-window ;; (unwind-protect ; 上手く動かないので一旦コメントアウト ;; (progn ;; (save-excursion ; バッファを変えるからsave-excursionでは戻せない ;; (with-current-buffer (current-buffer) ; これもダメ ;; スイッチ一覧をカレントバッファの隣に表示する (if (one-window-p) ; ウィンドウが分割されていなければ新たに分割 (split-window (selected-window) 80 t)) (other-window 1) (switch-to-buffer "*SE Switches*") ;;(get-buffer-create "*SE Switches*") (set-buffer "*SE Switches*") ;; (erase-buffer) (insert "効果音を挿入\n==========\n") (MZScr/insert-mzscr-switches se-switch-list nil) ; バッファに効果音一覧を項番付きで表示 (setq choice (completing-read "インデックスで選択: " se-option-list)) ;; 選択まで終えたので、元のフレーム構成に戻す (set-window-configuration config) (kill-buffer "*SE Switches*") ;;(switch-to-buffer cur-buf) ;;(set-window-configration config)) ;; choice番目の効果音番号を取得してバッファに挿入 (let ((i 0) (l se-switch-list)) (while (not (eq (string-to-number choice) i)) (setq l (cdr l) i (1+ i))) (insert (format "sw%d" (cadar l)))))))) (defun MZScr/insert-standing-picture () "立ち絵番号を挿入する" ;; 選択肢表示 ;; picファイル名リストから選択肢のリストを作成 (let ((choice) (options (mapcar 'car mzscr-pic-name-alist)) (names (mapcar 'cadr mzscr-pic-name-alist))) (let ((choice-sentence (mapconcat 'identity (cl-mapcar (lambda (s1 s2) (format "%s[%s]" s1 s2)) names options) " | "))) (setq choice (completing-read (format "%s :" choice-sentence) options))) ;; キャラクター選択終わり ;; 立ち絵画像表示 ;; スイッチ一覧をカレントバッファの隣に表示する (let ((config (current-window-configuration))) (if (one-window-p) (split-window (selected-window) 20 t) ; ウィンドウが1つだけなら新たに分割 (window-resize (selected-window) 40 t)) ; 2つ以上ならカレントウィンドウをリサイズ (other-window 1) (switch-to-buffer "*立ち絵*") (set-buffer "*立ち絵*") (let ((file-path (concat "~/.emacs.d/elisp/mz-scripter-mode/fig/" choice ".png"))) (let ((img (create-image file-path 'png nil))) (if img (insert-image img) (insert (format "画像を読み込めませんでした: %s" file-path))))) (setq choice (completing-read "表情番号を入力 :" '())) ;; 選択まで終えたので、元のフレーム構成に戻す (set-window-configuration config) (kill-buffer "*立ち絵*") (insert (format "se%s" choice))))) (defun MZScr/insert-staging () "演出を挿入する" ;; スイッチのリストから演出の一覧バッファを作る ;; (("富竹フラッシュ" 85) ("ナイスな演出" 86)) (let ((action-switch-list (cadr (assoc 'Action mzscr-switches)))) ; 演出一覧 (let ((action-option-list (mapcar (lambda (e) (format "%s" e)) (number-sequence 0 (1- (length action-switch-list)))))) ; 選択肢用のオプション (let ((choice) (config (current-window-configuration))) ;; スイッチ一覧をカレントバッファの隣に表示する (if (one-window-p) ; ウィンドウが分割されていなければ新たに分割 (split-window (selected-window) 80 t)) (other-window 1) (switch-to-buffer "*Action Switches*") (set-buffer "*Action Switches*") (insert "演出を挿入\n==========\n") (MZScr/insert-mzscr-switches action-switch-list nil) ; バッファに演出一覧を項番付きで表示 (setq choice (completing-read "インデックスで選択: " action-option-list)) ;; 選択まで終えたので、元のフレーム構成に戻す (set-window-configuration config) (kill-buffer "*Action Switches*") ;; choice番目の演出番号を取得してバッファに挿入 (let ((i 0) (l action-switch-list)) (while (not (eq (string-to-number choice) i)) (setq l (cdr l) i (1+ i))) (insert (format "sw%d" (cadar l)))))))) (defun MZScr/insert-mzscr-switches(switch-list by-actual-nump) "内部処理用:スイッチ番号と名前をバッファに挿入する by-actual-nump:tを指定すると、実際のスイッチ番号にて挿入 nilを指定すると、0から始まる項番にて挿入" (let ((i 0) (l switch-list) e) (while (not (eq i (length switch-list))) (setq i (1+ i) e (car l) l (cdr l)) (let ((name (car e)) ; "富竹フラッシュ" (num (cadr e))) ; 42 (if by-actual-nump (insert (format "[%s] %s\n" num name)) ; 実際のスイッチ番号 (insert (format "[%d] %s\n" (1- i) name))))))) ; 0から始まる項番 ;;;;;;;; JSON変換機能 ;;;; 処理用変数 ;; 現在処理中の行の種類 ;; キャラ名行:name, メモ行:memo, 注釈行:annotation, セリフ行:dialogue ;; メモ行は複数行に跨るため、メモ行の/*から*/までの間はメモ行用の変換を行う ;; 基本的に処理を終えたらnilに戻す ;; メモ行など複数行にまたがるものは戻さずそのままにする (setq mzscr-current-line-kind nil) ;; キャラ文字色 ;; 台本からjsonへの変換のアルゴリズム上、キャラの文字色はグローバル変数として保持 ;; キャラ名行から文字色を判定して、セリフ行の行頭に配置する (setq mzscr-text-color "\\\\c[0]") (defun MZScr/get-name (key) "キャラ辞書からキャラ名を取得" (let ((character-info (assoc key mzscr-character-info-alist))) (cadr character-info))) (defun MZScr/get-text-color (key) "キャラ辞書からキャラ文字色を取得" (let ((character-info (assoc key mzscr-character-info-alist))) (caddr character-info))) (defun MZScr/replace-se (arg) "テキスト中の表情番号をMZ形式に変換する。 se22 → \\SE[22] ※ JSON形式に併せてbackslashはふたつ 「se」と「1桁以上の数字」がくっ付いたもの検索し、1桁以上の数字は置換後の文字列にて利用(\\2のとこ)" (replace-regexp-in-string "\\(se\\)\\([0-9]+\\)" "\\\\\\\\SE[\\2]" arg)) (defun MZScr/replace-switch (arg) "テキスト中のスイッチ番号をMZ形式に変換する。 sw22 → \\+switch[22]" (replace-regexp-in-string "\\(sw\\)\\([0-9]+\\)" "\\\\\\\\+switch[\\2]" arg)) (defun MZScr/replace-ruby (arg) "テキスト中のルビをMZ形式に変換する rb文字,もじbr → \\rb[文字,もじ] 正規表現:5か所に分けて一致させる rb ','を除く1文字以上 ',' ','を除く1文字以上 br ※ M-x rb-builder で正規表現を確認しながら書ける ※ Emacs Lispの正規表現は、バックスラッシュが多くなる(エスケープがイケてないため)" (replace-regexp-in-string "\\(rr\\)\\([^,]+\\),\\([^,]+\\)\\(bb\\)" "\\\\\\\\r[\\2,\\3]" arg)) ;; (ruby-replacer "rb猫,ねこbr") ;; (ruby-replacer "rb猫,ねこbr test 一行に2回以上現れるパターンrb猫,ねこbr") (defun MZScr/replace-ctrlChar (arg) "テキスト中の制御文字をMZ形式に変換する" (let ((replacements '(("tb" . "\\\\\\\\{") ; 文字拡大, tb(Text Big) → \\{ ("ts" . "\\\\\\\\}") ; 文字縮小, ts(Text Small) → \\} ("te" . "\\\\\\\\!") ; リターン待ち, te(Text End) → \\! ("ws" . "\\\\\\\\.") ; 1/4s wait, ws(Wait Short) → \\. ("wl" . "\\\\\\\\|") ; 1s wait te(Wait Long) → \\| ("fnn" . "\\\\\\\\fn[n]") ; ネコフォント, fnn → \\fn[n] ("fng" . "\\\\\\\\fn[gag]") ; ギャグフォント, fng → \\fn[gag] ("fne" . "\\\\\\\\fn")))) ; フォント変更終了, fne → \\fn (dolist (pair replacements arg) (setq arg (replace-regexp-in-string (car pair) (cdr pair) arg))))) (defun MZScr/set-line-kind (arg) "argが何の行なのか設定" (cond ((MZScr/get-name arg) ; キャラ名がヒットしたらキャラ名行 (setq mzscr-current-line-kind 'NAME)) ((and (<= 1 (length arg)) (string= (substring arg 0 1) ";")) ; 先頭1文字目が;だったら注釈行 (setq mzscr-current-line-kind 'ANNOTATION)) ((and (<= 4 (length arg)) (string= (substring arg 0 4) "wait")) ; 先頭4文字がwaitだったらウェイト行 (setq mzscr-current-line-kind 'WAIT)) ((or ; メモ行は複数行にまたがる (eq mzscr-current-line-kind 'MEMO) ; メモ行が設定されてたらメモ行の途中 (string= arg "/*") ; メモ行の開始記号 (string= arg "*/")) ; メモ行の終了記号 (setq mzscr-current-line-kind 'MEMO)) ; メモ行 ((or ; スクリプト行は複数行にまたがる (eq mzscr-current-line-kind 'SCRIPT) ; スクリプト行が設定されてたらスクリプト行の途中 (and (<= 2 (length arg)) (string= (substring arg 0 2) "ss")) ; 先頭2文字がssだったらスクリプト行 (string= arg "cc")) ; スクリプト行の終了記号 (setq mzscr-current-line-kind 'SCRIPT)) ; スクリプト行 ((and (<= 6 (length arg)) (string= (substring arg 0 6) "action")) ; 先頭6文字がactionだったらアクション行 (setq mzscr-current-line-kind 'ACTION)) (t (setq mzscr-current-line-kind 'DIALOGUE)))) ; それ以外はセリフ行 (defun MZScr/script-line-to-json-line (arg) "台本の一行をMZの会話用JSONの一行に変換する。 行の種類に応じて変換処理を変える。 ・キャラ名行 NAME ・注釈行 ANNOTATION ・ウェイト行 WAIT ・アクション行(対応予定) ACTION ・セリフ行 DIALOGUE" (MZScr/set-line-kind arg) ; 引数行の種類を取得 (cond ((eq mzscr-current-line-kind 'NAME) ; キャラ名の変換処理 (setq mzscr-current-line-kind nil) ; 引数行の種類をnilに戻す (let ((template-str "{\"code\":101,\"indent\":0,\"parameters\":[\"\",0,0,2,\"%%NAME%%\"]},\n")) (let ((name (MZScr/get-name arg)) (text-color (MZScr/get-text-color arg))) ; キャラ名とキャラ文字色を取得 (setq mzscr-text-color text-color) ; キャラ文字色をセット ※ここでは使わない。セリフ行の変換処理にて行頭に配置する (princ (string-replace "%%NAME%%" name template-str))))) ((eq mzscr-current-line-kind 'ANNOTATION) ; 注釈行の変換処理 (setq mzscr-current-line-kind nil) ; 引数行の種類をnilに戻す (let ((template-str "{\"code\":108,\"indent\":0,\"parameters\":[\";;\"]},{\"code\":408,\"indent\":0,\"parameters\":[\";; %%LINE%%\"]},\n")) (princ (string-replace "%%LINE%%" arg template-str)))) ((eq mzscr-current-line-kind 'WAIT) ; ウェイト行の変換処理 (message "waitwaitwait") (setq mzscr-current-line-kind nil) ; 引数行の種類をnilに戻す (let ((template-str "{\"code\":230,\"indent\":0,\"parameters\":[%%VALUE%%]},\n") (value (string-replace "wait" "" (string-replace " " "" arg)))) ; ウェイトする値を取り出し (princ (string-replace "%%VALUE%%" value template-str)))) ((eq mzscr-current-line-kind 'MEMO) ; メモ行の変換処理 ;; 変換処理は何もしない (if (string= arg "*/") ; メモ行の終了記号 */ が来た場合のみ引数行の種類をnilに戻す (setq mzscr-current-line-kind nil))) ((eq mzscr-current-line-kind 'SCRIPT) ; スクリプト行の変換処理 (if (string= arg "cc") ;; スクリプト行の終了記号 cc が来た場合のみ引数行の種類をnilに戻す ;; また、ccの際は変換処理は行わない ;(progn (setq mzscr-current-line-kind nil) ;(princ "hogehoghoge")) (let ((template-str "{\"code\":%%CODE_NUM%%,\"indent\":0,\"parameters\":[\"%%LINE%%\"]},\n")) (if (and (<= 2 (length arg)) (string= (substring arg 0 2) "ss")) ;; ssから始まる行はcode:355, "ss "(ssと半角スペース) を取り除いてから置換 (let ((without-ss-arg (string-replace "ss" "" (string-replace "ss " "" arg)))) (princ (string-replace "%%CODE_NUM%%" "355" (string-replace "%%LINE%%" without-ss-arg template-str)))) ;; それ以外は655, argをそのまま置換 (princ (string-replace "%%CODE_NUM%%" "655" (string-replace "%%LINE%%" arg template-str))))))) ((eq mzscr-current-line-kind 'ACTION) ; アクション行の変換処理 (setq mzscr-current-line-kind nil) ; 引数行の種類をnilに戻す (concat arg "アクション用処理")) ((eq mzscr-current-line-kind 'DIALOGUE) ; セリフ行の変換処理 (setq mzscr-current-line-kind nil) ; 引数行の種類をnilに戻す ;; セリフ行の変換規則 ;; 1. キャラに応じて文字色を先頭に配置 ;; 2. "se"と1桁以上の数字が続く箇所をMZの表情番号に置換 se22→\\SE[22] ;; 3. "sw"と1桁以上の数字が続く箇所をMZのスイッチ番号に置換 sw→\\+switch[22] ;; 4. ルビを置換。rb文字,もじbr→\\rb[文字,もじ] ;; 5. 制御文字を置換。文字大(Text Big),"tb"→\\{ 文字小(Text Small) ts→\\} リターン待ち(Text End) te→\\! (let ((template-str "{\"code\":401,\"indent\":0,\"parameters\":[\" %%TEXT-COLOR%%%%LINE%%\"]},\n") (line-replaced (MZScr/replace-ruby (MZScr/replace-se (MZScr/replace-switch (MZScr/replace-ctrlChar arg)))))) ; セリフ行。制御文字、表情番号、スイッチ番号、ルビを置換 (princ (string-replace "%%TEXT-COLOR%%" mzscr-text-color (string-replace "%%LINE%%" line-replaced template-str))))) (t nil))) ;;(MZScr/script-line-to-json-line "tc") ;;(MZScr/script-line-to-json-line "; 注釈行です") ;;(MZScr/script-line-to-json-line "actionアクション行だよ") ;;(MZScr/script-line-to-json-line "se4tbtbぬにゃあ!te rr本気,マジbb tstsにゅおお・・・。te") ;;(MZScr/script-line-to-json-line "/*") ;;(MZScr/script-line-to-json-line "fff") ;;(MZScr/script-line-to-json-line "*/") (defun MZScr/script-list-to-json-list (script-list) "台本全行(リスト)をMZの会話用JSON(リスト)に変換" (mapcar 'MZScr/script-line-to-json-line script-list)) ;; (MZScr/script-list-to-json-list '("yc" "se4tbぬにゃあ!sw42te" "se16グーン・ウォーリアー号!te")) (defun mz-scripter-mode-exe-region () "リージョンからmzのマップjsonへ変換してウィンドウに書き出す" (interactive) ; interactiveの引数についてのとっかかり https://nitbit.hatenadiary.org/entry/20101103/1288781507 (let ((region-string (buffer-substring-no-properties (region-beginning) (region-end)))) (let ((region-string-list (remove "" (split-string region-string "\n")))) ;リージョンの各行からリストを作る。空行は除去 ;; (prin1 region-string-list) ;;(goto-char (point-max)) ;; バッファ末行に移動 ;;(insert "\n") (insert "\n") ;; 空行入れてから出力(コピペしやすいため!) (let ((config (current-window-configuration))) ;; 変換後のjsonをカレントウィンドウの隣に表示する (if (one-window-p) ; ウィンドウが分割されていなければ新たに分割 (split-window (selected-window) 80 t)) (other-window 1) (switch-to-buffer "*MZ Scripter Mode Output*") (set-buffer "*MZ Scripter Mode Output*") ;; mapc..リストの全要素に対して処理を実施、リストは返さない ;; mapcar..リストの全要素に対して処理を実施し、新しいリストを返す (mapc 'insert (MZScr/script-list-to-json-list region-string-list)) ;; 変換、書き出し (insert "\n"))))) ;; 最後に空行 (defun mz-scripter-mode-exe-buffer () "バッファからmzのマップjsonへ変換してウィンドウに書き出す" (interactive) (let ((buffer-string-list (remove "" (split-string (buffer-substring-no-properties (point-min) (point-max)) "\n")))) ;バッファからリストを作る。空行は除去 (let ((config (current-window-configuration))) ;; 変換後のjsonをカレントウィンドウの隣に表示する (if (one-window-p) ; ウィンドウが分割されていなければ新たに分割 (split-window (selected-window) 80 t)) (other-window 1) (switch-to-buffer "*MZ Scripter Mode Output*") (set-buffer "*MZ Scripter Mode Output*") (mapc 'insert (MZScr/script-list-to-json-list buffer-string-list)) ;; 変換、書き出し (insert "\n")))) (defun mz-scripter-mode-export-to-mz-buffer () (interactive) "バッファからmzのマップjsonファイルへエクスポートする" (let ((buffer-string-list ;バッファからリストを作る。空行は除去 (remove "" (split-string (buffer-substring-no-properties (point-min) (point-max)) "\n")))) (let ((script-json-text (mapconcat #'identity (MZScr/script-list-to-json-list buffer-string-list) "\t"))) ;台本からjson化 (let ((jsonbody ;Map用jsonのボディ (with-temp-buffer ; 一時バッファにファイルの内容を取得 (insert-file-contents mzscr-json-template-path) (goto-char (point-min)) (while (search-forward "%%MZSCR%%" nil t) ; 置き換え部分を台本jsonに置換する (replace-match script-json-text t t)) (buffer-string)))) ;; エクスポート先のファイルに書き出す (find-file mzscr-json-exportfile-path) (erase-buffer) (insert jsonbody) (save-buffer) (kill-buffer))))) ;; (x-popup-menu t '("Menu Title" ;; ("表情" ;; ("Item1-1" . 11) ;; ("Item1-2" . 12)) ;; ("効果音" ;; ("ツッコミ" . 42) ;; ("シャキーン" . 43) ;; ("ボヨン" . 44)) ;; ("演出" ;; ("富竹フラッシュ" . 21)))) (provide 'mz-scripter-mode) ;;; mz-scripter-mode.el ends here
(define-minor-mode mz-scripter-mode-highlight "キーワードを青色でハイライトするマイナーモード" :lighter " MZScr-hl" (if mz-scripter-mode-highlight ;; 正規表現の部分には変数は使えないらしい (font-lock-add-keywords nil '(("\\(se\\|sw\\)[0-9]+" . 'mz-scripter-mode-highlight-keyword-and-integer-face) ; se23 ("\\(wc\\|wh\\|tc\\|th\\|yc\\|yh\\|ac\\|ah\\|w\\|k\\|hachi\\|kuro\\|txt\\|iseki\\|iseki_fu1\\|iseki_fu2\\|iseki_fu3\\|konoe1\\|konoe2\\|king\\|jiiya\\|master\\)\n" . 'mz-scripter-mode-highlight-character-face) ; wc ("\\(rr\\)\\([^,]+\\),\\([^,]+\\)\\(bb\\)" . 'mz-scripter-mode-highlight-ruby-face) ; rr猫,ねこbb ;("/\\*\\(.\\|\n\\)*?\\*/" . 'mz-scripter-mode-highlight-memo-face) ; /*メモ行。改行を含む*/ ("/\\*\\(.\\|\n\\)*?\\*/" ; /*メモ行。改行を含む*/ (0 (progn (put-text-property (match-beginning 0) (match-end 0) 'font-lock-multiline t) ; 複数行にまたがるため、font-lock-multilineプロパティを使う 'mz-scripter-mode-highlight-memo-face))) ("ss\\(.\\|\n\\)*?cc" ; /*スクリプト行。改行を含む*/ (0 (progn (put-text-property (match-beginning 0) (match-end 0) 'font-lock-multiline t) ; 複数行にまたがるため、font-lock-multilineプロパティを使う 'mz-scripter-mode-highlight-script-face))) (";.+" . 'mz-scripter-mode-highlight-annotation-face) ; 注釈行 ("te\\|tb\\|ts\\|ws\\|wl\\|fnn\\|fng\\|fne\\|wait" . 'mz-scripter-mode-highlight-keyword-only-face) ; te ("\\(?:[^\x00-\x7F]\\)\\{31,\\}" . 'mz-scripter-mode-highlight-over30chars-face) ; 全角30文字超え ("^#[^#].*" (0 (when (mz-scripter-mode-highlight--inside-memo-block-p) 'mz-scripter-mode-highlight-h1-face) prepend)) ("^##[^#].*" (0 (when (mz-scripter-mode-highlight--inside-memo-block-p) 'mz-scripter-mode-highlight-h2-face) prepend)))) (font-lock-remove-keywords nil '(("\\(se\\|sw\\)[0-9]+" . 'mz-scripter-mode-highlight-keyword-and-integer-face) ("\\(wc\\|wh\\|tc\\|th\\|yc\\|yh\\|ac\\|ah\\|w\\|k\\|hachi\\|kuro\\|txt\\|iseki\\|iseki_fu1\\|iseki_fu2\\|iseki_fu3\\|konoe1\\|konoe2\\|king\\|jiiya\\|master\\)\n" . 'mz-scripter-mode-highlight-character-face) ("\\(rr\\)\\([^,]+\\),\\([^,]+\\)\\(bb\\)" . 'mz-scripter-mode-highlight-ruby-face) ("/\\*\\(.\\|\n\\)*?\\*/" . 'mz-scripter-mode-highlight-memo-face) ("ss\\(.\\|\n\\)*?cc" . 'mz-scripter-mode-highlight-script-face) (";.+" . 'mz-scripter-mode-highlight-annotation-face) ("te\\|tb\\|ts\\|ws\\|wl\\|fnn\\|fng\\|fne\\|wait" . 'mz-scripter-mode-highlight-keyword-only-face) ("\\(?:[^\x00-\x7F]\\)\\{31,\\}" . 'mz-scripter-mode-highlight-over30chars-face) ("^#[^#].*" (0 'mz-scripter-mode-highlight-h1-face prepend)) ("^##[^#].*" (0 'mz-scripter-mode-highlight-h2-face prepend)))) (font-lock-flush))) (defun mz-scripter-mode-highlight--inside-memo-block-p () "現在の行が /* ... */ の中にあるかを判定する。" (save-excursion (let ((pos (point))) (and (re-search-backward "/\\*" nil t) (re-search-forward "\\*/" nil t) (< pos (match-end 0)))))) (defface mz-scripter-mode-highlight-keyword-only-face '((t (:foreground "dark violet" :weight bold))) "キーワードのみのパターンのためのハイライトフェイス。") (defface mz-scripter-mode-highlight-keyword-and-integer-face '((t (:foreground "blue" :weight bold))) "キーワード+整数値のパターンのためのハイライトフェイス。") (defface mz-scripter-mode-highlight-character-face '((t (:foreground "black" :background "medium spring green"))) "キャラクター名のパターンのためのハイライトフェイス。") (defface mz-scripter-mode-highlight-ruby-face ;;'((t (:background "LightSkyBlue1" :foreground "firebrick3"))) '((t (:foreground "firebrick3"))) "ルビのパターンのためのハイライトフェイス。") (defface mz-scripter-mode-highlight-over30chars-face '((t (:background "RosyBrown1"))) "全角30文字を超過したパターンのためのハイライトフェイス。") (defface mz-scripter-mode-highlight-annotation-face '((t (:foreground "dark green" :weight bold))) "注釈行のハイライトフェイス。") (defface mz-scripter-mode-highlight-script-face '((t (:background "#FFFEC4" :underline (:color "blue4")))) "スクリプト行のハイライトフェイス。") (defface mz-scripter-mode-highlight-memo-face '((t (:foreground "RoyalBlue1" :background "light cyan"))) "メモ行のハイライトフェイス。") (defface mz-scripter-mode-highlight-h1-face '((t (:foreground "RoyalBlue1" :weight bold :height 2.0 :underline (:color "blue4")))) "メモ行内だけで使える、h1行のフェイス") (defface mz-scripter-mode-highlight-h2-face '((t (:foreground "RoyalBlue1" :weight bold :height 1.2))) "メモ行内だけで使える、h2行のフェイス") (defun enable-mz-scripter-highlight () "特定のメジャーモードにhookして、mz-scripter-mode-highlight を自動的に有効化するための関数" (mz-scripter-mode-highlight 1)) (provide 'mz-scripter-mode-highlight)
実際にEmacs上で書いている台本はこんな感じです。
呪文みたいなものが多いけど、MZのエディタに直接打ち込むよりは
こっちで書いた方が速く書けます。
/*
シナリオ行 アイデアやTODOなどのMZに反映しないが書き残したい事など
*/
;; 注釈行 こちらはMZの注釈としてゲームデータに反映する
tc
se15sw48サクシャが開発した
tbtbrr台本執筆,だいほんしっぴつbbtsts用メジャーモードの紹介です。
;; スクリプト行 立ち絵のポーズを回転しながらジャンプに変更
ssskit flip tc f 2
skit pose tc f jump 11
cc
tc
sw85sw43シナリオ台本書きがだいぶ効率化したらしいですよー!
この台本を
C-c !
でMZプロジェクトにエクスポートしてテストプレイした動画がコチラ
台本の基本的な文法は以下の通りに設定しました。
EBNFっぽく表記しましたが、自分用にテキトウに定義したものなので穴があると思います
台本の主な行について説明します
tc
キャラクター名の略称をメジャーモードのキャラ辞書に登録しておくことで、MZへのエクスポート時にキャラ辞書で対応する名前に変換します。
今回の例では、略称を「tc」を書いておくことで、ゲームの主人公名「たま」へ変換しています。
また、私の作品ではキャラクター毎にテキストの文字色を変更しています。
この例では、キャラクターのたまは青色と対応させているため、エクスポート後の動画ではテキストが青色で表示されてます。
se15sw48サクシャが開発した
tbtbrr台本執筆,だいほんしっぴつbbtsts用メジャーモードの紹介です。
sw85sw43シナリオ台本書きがだいぶ効率化したらしいですよー!
キャラクターのセリフ文章と一緒に、メジャーモードで定義した専用のコマンドによってセリフと共に実行する各種制御を指定します。
表:指定できる制御
| コマンド | 概要 | サンプル |
|---|---|---|
| se表情番号 | 立ち絵の表情番号を指定する | se1 |
| sw演出番号 | 効果音、演出の番号を指定する | sw2 |
| tb | 文字サイズの拡大 | tb |
| ts | 文字サイズの縮小 | ts |
| rrテキスト,ルビbb | テキストにルビを振る | rr猫,ニャンコbb |
ばけねこたちが元気に謎を解いていく、謎解きアドベンチャーフリーゲームを開発、公開しています。
今年の11月に第2作目が完成したのでぜひプレイしてみてね!
2025/12/1現在 WindowsのDL版のみ公開中
Webブラウザ版を近日公開予定
copyright© 2025 tachiyama, BAKENEKO Detective Agency All Rights Reserved.
